From 4e32f2e98606fbfa8a2243b03e608825a3ba9fe8 Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 25 Sep 2017 15:40:47 +0800 Subject: [PATCH 001/208] [update] update --- .gitignore | 23 + api.md | 74 + bin/www | 92 + logs/.gitkeep | 0 package-lock.json | 3099 +++++++++++++++++++++++++ package.json | 29 + server/app.js | 50 + server/config/development.js | 20 + server/config/index.js | 49 + server/config/production.js | 21 + server/config/test.js | 7 + server/controller/article/index.js | 8 + server/middleware/error.js | 27 + server/middleware/index.js | 10 + server/middleware/response.js | 33 + server/model/index.js | 8 + server/model/schema/article.schema.js | 45 + server/model/schema/comment.schema.js | 8 + server/model/schema/log.schema.js | 8 + server/model/schema/tag.schema.js | 19 + server/model/schema/user.schema.js | 8 + server/mongo.js | 13 + server/routes/backend.js | 8 + server/routes/frontend.js | 8 + server/routes/index.js | 20 + test/.gitkeep | 0 26 files changed, 3687 insertions(+) create mode 100644 .gitignore create mode 100644 api.md create mode 100755 bin/www create mode 100644 logs/.gitkeep create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/app.js create mode 100644 server/config/development.js create mode 100644 server/config/index.js create mode 100644 server/config/production.js create mode 100644 server/config/test.js create mode 100644 server/controller/article/index.js create mode 100644 server/middleware/error.js create mode 100644 server/middleware/index.js create mode 100644 server/middleware/response.js create mode 100644 server/model/index.js create mode 100644 server/model/schema/article.schema.js create mode 100644 server/model/schema/comment.schema.js create mode 100644 server/model/schema/log.schema.js create mode 100644 server/model/schema/tag.schema.js create mode 100644 server/model/schema/user.schema.js create mode 100644 server/mongo.js create mode 100644 server/routes/backend.js create mode 100644 server/routes/frontend.js create mode 100644 server/routes/index.js create mode 100644 test/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7813b29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Coverage directory used by tools like istanbul +coverage + +# Debug log from npm +npm-debug.log + +# IDEA +.vscode +.idea + +.DS_Store diff --git a/api.md b/api.md new file mode 100644 index 0000000..0d6e94e --- /dev/null +++ b/api.md @@ -0,0 +1,74 @@ +## Api + +prefix: /api + +### 文章 + +> GET /frontend/articles 前台-文章列表 + +> GET /frontend/articles/:id 前台-文章详情 + +> GET /backend/articles 后台-文章列表 + +> GET /backend/articles/:id 后台-文章详情 + +> POST /backend/articles 后台-创建文章 + +> PATCH /backend/articles/:id 后台-修改文章 + +> DELETE /backend/articles/:id 后台-删除文章 + + +### 标签 + +> GET /frontend/tags 前台-标签列表 + +> GET /frontend/tags/:id 前台-标签详情 + +> GET /backend/tags 后台-标签列表 + +> GET /backend/tags/:id 后台-标签详情 + +> POST /backend/tags 后台-创建标签 + +> PATCH /backend/tags/:id 后台-修改标签 + +> DELETE /backend/tags/:id 后台-删除标签 + +### 评论 + +> GET /frontend/comments 前台-评论列表 + +> GET /frontend/comments/:id 前台-评论详情 + +> POST /frontend/comments 前台-发布评论 + +> GET /backend/comments 后台-评论列表 + +> GET /backend/comments/:id 后台-评论详情 + +> POST /backend/comments 后台-创建评论 + +> PATCH /backend/comments/:id 后台-修改评论 + +> DELETE /backend/comments/:id 后台-删除评论 + +### 全站配置 + +> GET /frontend/option 前台-全站配置 + +> GET /backend/option 后台-全站配置 + +> PATCH /backend/option 后台-修改全站配置 + +### 音乐 + +> GET /fronend/music/songs 前台-歌曲列表 + +> GET /fronend/music/songs/:id 前台-歌曲详情 + +> GET /fronend/music/songs/:id/url 前台-歌曲地址 + +> GET /fronend/music/songs/:id/lyric 前台-歌曲歌词 + +> GET /fronend/music/songs/:id/cover 前台-歌曲封面 diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..609910e --- /dev/null +++ b/bin/www @@ -0,0 +1,92 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +const http = require('http') +const app = require('../server/app') +const debug = require('debug')(require('../package.json').name) +const config = require('../server/config') + +debug.enabled = true + +/** + * Get port from environment and store in Express. + */ + +const port = normalizePort(config.port) + +/** + * Create HTTP server. + */ + +const server = http.createServer(app.callback()) + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port) +server.on('error', onError) +server.on('listening', onListening) + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + const port = parseInt(val, 10) + + if (isNaN(port)) { + // named pipe + return val + } + + if (port >= 0) { + // port number + return port + } + + return false +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error + } + + const bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges') + process.exit(1) + break + case 'EADDRINUSE': + console.error(bind + ' is already in use') + process.exit(1) + break + default: + throw error + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + const addr = server.address() + const bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port + debug('Listening on ' + bind) +} diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fadea0b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3099 @@ +{ + "name": "jooger.me-server", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", + "dev": true + }, + "accepts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "requires": { + "mime-types": "2.1.17", + "negotiator": "0.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "2.1.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "binary-extensions": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", + "integrity": "sha1-muuabF6IY4qtFx4Wf1kAq+JINdA=", + "dev": true + }, + "bluebird": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz", + "integrity": "sha1-AkpVFylTCIV/FPkfEQb8O1VfRGs=" + }, + "boxen": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.2.1.tgz", + "integrity": "sha1-DxHn/jRO25OXl3/BPt5/ZNlWSB0=", + "dev": true, + "requires": { + "ansi-align": "2.0.0", + "camelcase": "4.1.0", + "chalk": "2.1.0", + "cli-boxes": "1.0.0", + "string-width": "2.1.1", + "term-size": "1.2.0", + "widest-line": "1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "chalk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", + "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.4.0" + } + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "bson": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", + "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "bunyan": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", + "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", + "requires": { + "dtrace-provider": "0.6.0", + "mv": "2.1.1", + "safe-json-stringify": "1.0.4" + } + }, + "bytes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "capture-stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.1.2", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "co-body": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-4.2.0.tgz", + "integrity": "sha1-dN8g+nMmISXcRUgq8E40LqjbNRU=", + "requires": { + "inflation": "2.0.0", + "qs": "4.0.0", + "raw-body": "2.1.7", + "type-is": "1.6.15" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "color-convert": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", + "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "compressible": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.11.tgz", + "integrity": "sha1-FnGKdd4oPtjmBAQWJaIGRYZ5fYo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "configstore": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.1.tgz", + "integrity": "sha512-5oNkD/L++l0O6xGXxb1EWS7SivtjfGQlRyxJsYgE0Z495/L81e2h4/d3r969hoPXuFItzNOKMtsXgYG4c7dYvw==", + "dev": true, + "requires": { + "dot-prop": "4.2.0", + "graceful-fs": "4.1.11", + "make-dir": "1.0.0", + "unique-string": "1.0.0", + "write-file-atomic": "2.3.0", + "xdg-basedir": "3.0.0" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookies": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.1.tgz", + "integrity": "sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=", + "requires": { + "depd": "1.1.1", + "keygrip": "1.0.2" + } + }, + "copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "1.0.0" + } + }, + "cross-env": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.0.5.tgz", + "integrity": "sha1-Q4PTZNlmCHPdGFs5ivO/717//vM=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "is-windows": "1.0.1" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "1.0.1" + } + }, + "dtrace-provider": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", + "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", + "optional": true, + "requires": { + "nan": "2.7.0" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "error-inject": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" + }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true, + "requires": { + "duplexer": "0.1.1", + "from": "0.1.7", + "map-stream": "0.1.0", + "pause-stream": "0.0.11", + "split": "0.3.3", + "stream-combiner": "0.0.4", + "through": "2.3.8" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "fsevents": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", + "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.7.0", + "node-pre-gyp": "0.6.36" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true, + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true, + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "dev": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.36", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true, + "dev": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + } + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "3.0.2", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "is-redirect": "1.0.0", + "is-retry-allowed": "1.1.0", + "is-stream": "1.1.0", + "lowercase-keys": "1.0.0", + "safe-buffer": "5.1.1", + "timed-out": "4.0.1", + "unzip-response": "2.0.1", + "url-parse-lax": "1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "hooks-fixed": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz", + "integrity": "sha1-oB2JTVKsf2WZu7H2PfycQR33DLo=" + }, + "http-assert": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.3.0.tgz", + "integrity": "sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=", + "requires": { + "deep-equal": "1.0.1", + "http-errors": "1.6.2" + } + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + } + }, + "humanize-number": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/humanize-number/-/humanize-number-0.0.2.tgz", + "integrity": "sha1-EcCvakcWQ2M1iFiASPF5lUFInBg=" + }, + "iconv-lite": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=" + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.10.0" + } + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-generator-function": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.6.tgz", + "integrity": "sha1-nnFlPNFf/zQcecQVFGChMdMen8Q=" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-windows": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.1.tgz", + "integrity": "sha1-MQ23D3QtJZoWo2kgK1GvhCMzENk=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "kareem": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz", + "integrity": "sha1-4+QQHZ3P3imXadr0tNtk2JXRdEg=" + }, + "keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + }, + "koa": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.3.0.tgz", + "integrity": "sha1-nh6OTaQBg5xXuFJ+rcV/dhJ1Vac=", + "requires": { + "accepts": "1.3.4", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookies": "0.7.1", + "debug": "2.6.9", + "delegates": "1.0.0", + "depd": "1.1.1", + "destroy": "1.0.4", + "error-inject": "1.0.0", + "escape-html": "1.0.3", + "fresh": "0.5.2", + "http-assert": "1.3.0", + "http-errors": "1.6.2", + "is-generator-function": "1.0.6", + "koa-compose": "4.0.0", + "koa-convert": "1.2.0", + "koa-is-json": "1.0.0", + "mime-types": "2.1.17", + "on-finished": "2.3.0", + "only": "0.0.2", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "type-is": "1.6.15", + "vary": "1.1.2" + } + }, + "koa-bodyparser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-3.2.0.tgz", + "integrity": "sha1-uRbeF+IDn+gmUEgZc9fClPELVxk=", + "requires": { + "co-body": "4.2.0" + } + }, + "koa-bunyan-logger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-bunyan-logger/-/koa-bunyan-logger-2.0.0.tgz", + "integrity": "sha1-TtkDR+mHhJ9JU/kVXeRvl/WFRM0=", + "requires": { + "bunyan": "1.5.1", + "on-finished": "2.1.1", + "uuid": "3.1.0" + }, + "dependencies": { + "ee-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", + "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" + }, + "on-finished": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", + "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=", + "requires": { + "ee-first": "1.1.0" + } + } + } + }, + "koa-compose": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.0.0.tgz", + "integrity": "sha1-KAClE9nDYe8NY4UrA45Pby1adzw=" + }, + "koa-compress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-compress/-/koa-compress-2.0.0.tgz", + "integrity": "sha1-e36ykhuEd0a14SK6n1zYpnHo6jo=", + "requires": { + "bytes": "2.4.0", + "compressible": "2.0.11", + "koa-is-json": "1.0.0", + "statuses": "1.3.1" + } + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "4.6.0", + "koa-compose": "3.2.1" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "1.3.0" + } + } + } + }, + "koa-is-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, + "koa-json": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/koa-json/-/koa-json-2.0.2.tgz", + "integrity": "sha1-Nq8U5uofXWRtfESihXAcb4Wk/eQ=", + "requires": { + "koa-is-json": "1.0.0", + "streaming-json-stringify": "3.1.0" + } + }, + "koa-logger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/koa-logger/-/koa-logger-2.0.1.tgz", + "integrity": "sha1-PuQvRXxA+01KGaExyAIlBLWyMrg=", + "requires": { + "bytes": "1.0.0", + "chalk": "1.1.3", + "humanize-number": "0.0.2", + "passthrough-counter": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", + "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=" + } + } + }, + "koa-onerror": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/koa-onerror/-/koa-onerror-1.3.1.tgz", + "integrity": "sha1-pbVh4ch+8yieQwF0bgE4LyT/viI=", + "requires": { + "copy-to": "2.0.1", + "swig": "1.4.2" + } + }, + "koa-router": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.2.1.tgz", + "integrity": "sha1-tApKs8attLQIld69AKnGQDBOMDk=", + "requires": { + "debug": "2.6.9", + "http-errors": "1.6.2", + "koa-compose": "3.2.1", + "methods": "1.1.2", + "path-to-regexp": "1.7.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "1.3.0" + } + } + } + }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "4.0.1" + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._createassigner": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", + "dev": true, + "requires": { + "lodash._bindcallback": "3.0.1", + "lodash._isiterateecall": "3.0.9", + "lodash.restparam": "3.6.1" + } + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.assign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", + "dev": true, + "requires": { + "lodash._baseassign": "3.2.0", + "lodash._createassigner": "3.1.1", + "lodash.keys": "3.1.2" + } + }, + "lodash.defaults": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", + "integrity": "sha1-xzCLGNv4vJNy1wGnNJPGEZK9Liw=", + "dev": true, + "requires": { + "lodash.assign": "3.2.0", + "lodash.restparam": "3.6.1" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "dev": true + }, + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "dev": true + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "make-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.0.0.tgz", + "integrity": "sha1-l6ARdR6R3YfPre9Ygy67BJNt6Xg=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true + } + } + }, + "mongodb": { + "version": "2.2.31", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.31.tgz", + "integrity": "sha1-GUBEXGYeGSF7s7+CRdmFSq71SNs=", + "requires": { + "es6-promise": "3.2.1", + "mongodb-core": "2.1.15", + "readable-stream": "2.2.7" + }, + "dependencies": { + "es6-promise": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", + "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" + }, + "readable-stream": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", + "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + } + } + }, + "mongodb-core": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.15.tgz", + "integrity": "sha1-hB9TuH//9MdFgYnDXIroJ+EWl2Q=", + "requires": { + "bson": "1.0.4", + "require_optional": "1.0.1" + } + }, + "mongoose": { + "version": "4.11.12", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.11.12.tgz", + "integrity": "sha512-41odmaImJVAoY/qg6gZ1Dn60qvFHIny01c7N58OlNPHBifzT6qWuiArtev7OYYE1ssPY7YvoX1hCbvZEUZfPnQ==", + "requires": { + "async": "2.1.4", + "bson": "1.0.4", + "hooks-fixed": "2.0.0", + "kareem": "1.5.0", + "mongodb": "2.2.31", + "mpath": "0.3.0", + "mpromise": "0.5.5", + "mquery": "2.3.1", + "ms": "2.0.0", + "muri": "1.2.2", + "regexp-clone": "0.0.1", + "sliced": "1.0.1" + }, + "dependencies": { + "async": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", + "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=", + "requires": { + "lodash": "4.17.4" + } + } + } + }, + "mongoose-paginate": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/mongoose-paginate/-/mongoose-paginate-5.0.3.tgz", + "integrity": "sha1-165J7Vv2Tx9692IOqGW2cFjFU3E=", + "requires": { + "bluebird": "3.0.5" + }, + "dependencies": { + "bluebird": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.0.5.tgz", + "integrity": "sha1-L/nQfJs+2ynW0oD+B1KDZefs05I=" + } + } + }, + "mpath": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz", + "integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q=" + }, + "mpromise": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz", + "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" + }, + "mquery": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.1.tgz", + "integrity": "sha1-mrNnSXFIAP8LtTpoHOS8TV8HyHs=", + "requires": { + "bluebird": "2.10.2", + "debug": "2.6.8", + "regexp-clone": "0.0.1", + "sliced": "0.0.5" + }, + "dependencies": { + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "requires": { + "ms": "2.0.0" + } + }, + "sliced": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", + "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "muri": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/muri/-/muri-1.2.2.tgz", + "integrity": "sha1-YxmBMmUNsIoEzHnM0A3Tia/SYxw=" + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "ncp": "2.0.0", + "rimraf": "2.4.5" + } + }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=", + "optional": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "nodemon": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.12.1.tgz", + "integrity": "sha1-mWpW3EnZ8Wu/G3ik3gjxNjSzh40=", + "dev": true, + "requires": { + "chokidar": "1.7.0", + "debug": "2.6.9", + "es6-promise": "3.3.1", + "ignore-by-default": "1.0.1", + "lodash.defaults": "3.1.2", + "minimatch": "3.0.4", + "ps-tree": "1.1.0", + "touch": "3.1.0", + "undefsafe": "0.0.3", + "update-notifier": "2.2.0" + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1.1.0" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "0.0.10", + "wordwrap": "0.0.3" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "6.7.1", + "registry-auth-token": "3.3.1", + "registry-url": "3.1.0", + "semver": "5.4.1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "passthrough-counter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passthrough-counter/-/passthrough-counter-1.0.0.tgz", + "integrity": "sha1-GWfZ5m2lcrXAI8eH2xEqOHqxZvo=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "ps-tree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", + "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", + "dev": true, + "requires": { + "event-stream": "3.3.4" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "qs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz", + "integrity": "sha1-wx2bdOwn33XlQ6hseHKO2NRiNgc=" + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "raw-body": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", + "integrity": "sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q=", + "requires": { + "bytes": "2.4.0", + "iconv-lite": "0.4.13", + "unpipe": "1.0.0" + } + }, + "rc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", + "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "dev": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regexp-clone": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", + "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" + }, + "registry-auth-token": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.1.tgz", + "integrity": "sha1-+w0yie4Nmtosu1KvXf5mywcNMAY=", + "dev": true, + "requires": { + "rc": "1.2.1", + "safe-buffer": "5.1.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "1.2.1" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "2.0.0", + "semver": "5.4.1" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "6.0.4" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safe-json-stringify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz", + "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", + "optional": true + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "5.4.1" + } + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, + "source-map": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "integrity": "sha1-p8/omux7FoLDsZjQrPtH19CQVms=", + "requires": { + "amdefine": "1.0.1" + } + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "requires": { + "duplexer": "0.1.1" + } + }, + "streaming-json-stringify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/streaming-json-stringify/-/streaming-json-stringify-3.1.0.tgz", + "integrity": "sha1-gCAEN6mTzDnE/gAmO3s7kDrIevU=", + "requires": { + "json-stringify-safe": "5.0.1", + "readable-stream": "2.3.3" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "swig": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/swig/-/swig-1.4.2.tgz", + "integrity": "sha1-QIXKBFM2kQS11IPihBs5t64aq6U=", + "requires": { + "optimist": "0.6.1", + "uglify-js": "2.4.24" + } + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "0.7.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "1.0.10" + } + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.17" + } + }, + "uglify-js": { + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "integrity": "sha1-+tV1XB4Vd2WLsG/5q25UjJW+vW4=", + "requires": { + "async": "0.2.10", + "source-map": "0.1.34", + "uglify-to-browserify": "1.0.2", + "yargs": "3.5.4" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=" + }, + "undefsafe": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-0.0.3.tgz", + "integrity": "sha1-7Mo6A+VrmvFzhbqsgSrIO5lKli8=", + "dev": true + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "1.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, + "update-notifier": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.2.0.tgz", + "integrity": "sha1-G1g3z5DAc22IYncytmHBOPht5y8=", + "dev": true, + "requires": { + "boxen": "1.2.1", + "chalk": "1.1.3", + "configstore": "3.1.1", + "import-lazy": "2.1.0", + "is-npm": "1.0.0", + "latest-version": "3.1.0", + "semver-diff": "2.1.0", + "xdg-basedir": "3.0.0" + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "1.0.4" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "widest-line": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz", + "integrity": "sha1-DAnIXCqUaD0Nfq+O4JfVZL8OEFw=", + "dev": true, + "requires": { + "string-width": "1.0.2" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "signal-exit": "3.0.2" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "integrity": "sha1-2K/49mXpTDS9JZvevRv68N3TU2E=", + "requires": { + "camelcase": "1.2.1", + "decamelize": "1.2.0", + "window-size": "0.1.0", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a1cd516 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "jooger.me-server", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "cross-env NODE_ENV=development ./node_modules/.bin/nodemon bin/www", + "debug": "cross-env NODE_ENV=development ./node_modules/.bin/nodemon --inspect bin/www", + "pm2": "pm2 start bin/www --name='jooger.me-server'", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "debug": "^2.6.9", + "koa": "^2.2.0", + "koa-bodyparser": "^3.2.0", + "koa-bunyan-logger": "^2.0.0", + "koa-compress": "^2.0.0", + "koa-json": "^2.0.2", + "koa-logger": "^2.0.1", + "koa-onerror": "^1.2.1", + "koa-router": "^7.1.1", + "lodash": "^4.17.4", + "mongoose": "^4.11.12", + "mongoose-paginate": "^5.0.3" + }, + "devDependencies": { + "cross-env": "^5.0.5", + "nodemon": "^1.8.1" + } +} diff --git a/server/app.js b/server/app.js new file mode 100644 index 0000000..3afbf02 --- /dev/null +++ b/server/app.js @@ -0,0 +1,50 @@ +/** + * @desc Server entry + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const Koa = require('koa') +const json = require('koa-json') +const logger = require('koa-logger') +const compress = require('koa-compress') +const onerror = require('koa-onerror') +const bodyparser = require('koa-bodyparser') +const koaBunyanLogger = require('koa-bunyan-logger') +const middlewares = require('./middleware') + +const app = new Koa() + +// error handler +onerror(app) + +// middlewares +app.use(bodyparser({ + enableTypes:['json', 'form', 'text'] +})) +app.use(json()) +app.use(logger()) +app.use(koaBunyanLogger()) +app.use(compress()) +app.use(middlewares.response) +app.use(middlewares.error) + +// logger +app.use(async (ctx, next) => { + const start = new Date() + await next() + const ms = new Date() - start + console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) +}) + +// routes +require('./routes')(app) + +// error-handling +app.on('error', (err, ctx) => { + console.error('server error', err, ctx) +}); + +module.exports = app diff --git a/server/config/development.js b/server/config/development.js new file mode 100644 index 0000000..b9a3885 --- /dev/null +++ b/server/config/development.js @@ -0,0 +1,20 @@ +/** + * @desc 开发环境配置 + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const packageInfo = require('../../package.json') + +module.exports = { + mongo: { + uri: `mongodb://127.0.0.1/${packageInfo.name}-dev` + }, + auth: { + cookie: { + maxAge: 60000 * 60 * 24 * 365 + } + } +} diff --git a/server/config/index.js b/server/config/index.js new file mode 100644 index 0000000..10c234d --- /dev/null +++ b/server/config/index.js @@ -0,0 +1,49 @@ +/** + * @desc Config entry + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const path = require('path') +const _ = require('lodash') +const packageInfo = require('../../package.json') + +const baseConfig = { + env: process.env.NODE_ENV, + root: path.resolve(__dirname, '../../'), + port: process.env.PORT || 3000, + codeMap: { + '-1': 'fail', + '200': 'success', + '401': 'token expired', + '403': 'forbidden', + '500': 'server error', + '10001': 'params error' + }, + mongo: { + useMongoClient: true + }, + // TODO: Redis + redis: {}, + auth: { + cookie: { + name: 'JOOGER_AUTH' + }, + secretKey: `${packageInfo.name} ${packageInfo.version}`, + // token过期时间 + expired: 60 * 60 * 24 * 365, + defaultName: 'admin', + defaultPassword: 'admin', + // 允许请求的域名 + allowedOrigins: [ + 'jooger.me', + 'www.jooger.me', + 'blog.jooger.me', + 'admin.jooger.me' + ] + } +} + +module.exports = _.merge(baseConfig, require(`./${process.env.NODE_ENV}`)) diff --git a/server/config/production.js b/server/config/production.js new file mode 100644 index 0000000..e6f577e --- /dev/null +++ b/server/config/production.js @@ -0,0 +1,21 @@ +/** + * @desc 开发环境配置 + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const packageInfo = require('../../package.json') + +module.exports = { + mongo: { + uri: `mongodb://127.0.0.1/${packageInfo.name}` + }, + auth: { + cookie: { + domain: '.jooger.me', + maxAge: 60000 * 60 * 24 * 365 + } + } +} diff --git a/server/config/test.js b/server/config/test.js new file mode 100644 index 0000000..cf2a5df --- /dev/null +++ b/server/config/test.js @@ -0,0 +1,7 @@ +/** + * @desc 测试环境配置 + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' diff --git a/server/controller/article/index.js b/server/controller/article/index.js new file mode 100644 index 0000000..97d96fa --- /dev/null +++ b/server/controller/article/index.js @@ -0,0 +1,8 @@ +/** + * @desc Article controller + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + diff --git a/server/middleware/error.js b/server/middleware/error.js new file mode 100644 index 0000000..e1b7e66 --- /dev/null +++ b/server/middleware/error.js @@ -0,0 +1,27 @@ +/** + * @desc Error monitor + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +module.exports = async (ctx, next) => { + try { + await next() + } catch (err) { + const code = err.status || 500 + ctx.fail(code) + ctx.status = code + + if (code === 500) { + ctx.log.error( + { req: ctx.req, err }, + ' --> %s %s %d', + ctx.request.method, + ctx.request.originalUrl, + ctx.status + ) + } + } +} \ No newline at end of file diff --git a/server/middleware/index.js b/server/middleware/index.js new file mode 100644 index 0000000..ac51561 --- /dev/null +++ b/server/middleware/index.js @@ -0,0 +1,10 @@ +/** + * @desc Middleware Entry + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +exports.error = require('./error') +exports.response = require('./response') diff --git a/server/middleware/response.js b/server/middleware/response.js new file mode 100644 index 0000000..db4afaf --- /dev/null +++ b/server/middleware/response.js @@ -0,0 +1,33 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const config = require('../config') + +module.exports = async (ctx, next) => { + ctx.success = (data = null) => { + ctx.status = 200 + ctx.body = { + code: 200, + success: true, + message: config.codeMap('200'), + data + } + } + + ctx.fail = (code = -1, message = config.codeMap['-1'], data = null) => { + ctx.status = 200 + ctx.send(200, { + code, + success: false, + message, + data + }) + } + + await next() +} diff --git a/server/model/index.js b/server/model/index.js new file mode 100644 index 0000000..ce73b00 --- /dev/null +++ b/server/model/index.js @@ -0,0 +1,8 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + diff --git a/server/model/schema/article.schema.js b/server/model/schema/article.schema.js new file mode 100644 index 0000000..54c860a --- /dev/null +++ b/server/model/schema/article.schema.js @@ -0,0 +1,45 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate') + +const articleSchema = new mongoose.Schema({ + // 文章标题 + title: { type: String, required: true }, + // 文章关键字(FOR SEO) + keywords: [{ type: String }], + // 文章摘要 (FOR SEO) + description: { type: String, default: '' }, + // 文章原始markdown内容 + content: { type: String, required: true, validate: /\S+/ }, + // markdown渲染后的htmln内容 + renderedContent: { type: String, required: true, validate: /\S+/ }, + // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) + thumb: { uid: String, title: { type: String, default: '' }, url: { type: String, default: '' }, size: Number }, + // 文章状态 ( 0 草稿 | 1 已发布 ) + state: { type: Number, default: 0 }, + // 创建日期 + createdAt: { type: Date, default: Date.now }, + // 更新日期 + updatedAt: { type: Date, default: Date.now }, + // 发布日期 + publishedAt: { type: Date, default: Date.now }, + // 标签 + tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], + // 文章元数据 (浏览量, 喜欢数, 评论数) + meta: { + pvs: { type: Number, default: 0 }, + ups: { type: Number, default: 0 }, + comments: { type: Number, default: 0 } + } +}) + +articleSchema.plugin(mongoosePaginate) + +module.exports = articleSchema diff --git a/server/model/schema/comment.schema.js b/server/model/schema/comment.schema.js new file mode 100644 index 0000000..ce73b00 --- /dev/null +++ b/server/model/schema/comment.schema.js @@ -0,0 +1,8 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + diff --git a/server/model/schema/log.schema.js b/server/model/schema/log.schema.js new file mode 100644 index 0000000..ce73b00 --- /dev/null +++ b/server/model/schema/log.schema.js @@ -0,0 +1,8 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + diff --git a/server/model/schema/tag.schema.js b/server/model/schema/tag.schema.js new file mode 100644 index 0000000..6c58032 --- /dev/null +++ b/server/model/schema/tag.schema.js @@ -0,0 +1,19 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const mongoose = require('mongoose') + +const tagSchema = new mongoose.Schema({ + name: { type: String, required: true }, + description: String, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, + forbidden: { type: Boolean, default: false } +}) + +module.exports = tagSchema diff --git a/server/model/schema/user.schema.js b/server/model/schema/user.schema.js new file mode 100644 index 0000000..ce73b00 --- /dev/null +++ b/server/model/schema/user.schema.js @@ -0,0 +1,8 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + diff --git a/server/mongo.js b/server/mongo.js new file mode 100644 index 0000000..e67a978 --- /dev/null +++ b/server/mongo.js @@ -0,0 +1,13 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const mongoose = require('mongoose') +const config = require('./config/env') + +mongoose.Promise = global.Promise + diff --git a/server/routes/backend.js b/server/routes/backend.js new file mode 100644 index 0000000..ce73b00 --- /dev/null +++ b/server/routes/backend.js @@ -0,0 +1,8 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + diff --git a/server/routes/frontend.js b/server/routes/frontend.js new file mode 100644 index 0000000..ce73b00 --- /dev/null +++ b/server/routes/frontend.js @@ -0,0 +1,8 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + diff --git a/server/routes/index.js b/server/routes/index.js new file mode 100644 index 0000000..4b6d066 --- /dev/null +++ b/server/routes/index.js @@ -0,0 +1,20 @@ +/** + * @desc Routes entry + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const Router = require('koa-router') +const frontend = require('./frontend') +const backend = require('./backend') +const router = new Router({ + prefix: '/api' +}) + +module.exports = app => { + // router.use('/frontend', frontend.routes(), frontend.allowedMethods()) + // router.use('/backend', backend.routes(), backend.allowedMethods()) + app.use(router.routes(), router.allowedMethods()) +} diff --git a/test/.gitkeep b/test/.gitkeep new file mode 100644 index 0000000..e69de29 From 430e94206cf3cee340304b9027fd60db7958a7b9 Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 25 Sep 2017 18:50:29 +0800 Subject: [PATCH 002/208] [add] add auth middleware --- package-lock.json | 165 ++++++++++++++++-- package.json | 3 + server/app.js | 15 +- server/config/development.js | 2 +- server/config/index.js | 7 +- server/config/production.js | 2 +- server/controller/article.js | 37 ++++ server/controller/article/index.js | 8 - server/middleware/auth.js | 59 +++++++ server/middleware/index.js | 1 + server/middleware/response.js | 10 +- server/model/index.js | 36 +++- server/model/schema/admin.js | 24 +++ .../schema/{article.schema.js => article.js} | 1 + server/model/schema/comment.js | 48 +++++ server/model/schema/index.js | 14 ++ .../schema/{comment.schema.js => log.js} | 0 .../model/schema/{log.schema.js => option.js} | 2 +- server/model/schema/{tag.schema.js => tag.js} | 0 server/model/schema/user.schema.js | 8 - server/mongo.js | 13 +- server/routes/backend.js | 8 + server/routes/frontend.js | 7 + server/routes/index.js | 16 +- 24 files changed, 419 insertions(+), 67 deletions(-) create mode 100644 server/controller/article.js delete mode 100644 server/controller/article/index.js create mode 100644 server/middleware/auth.js create mode 100644 server/model/schema/admin.js rename server/model/schema/{article.schema.js => article.js} (97%) create mode 100644 server/model/schema/comment.js create mode 100644 server/model/schema/index.js rename server/model/schema/{comment.schema.js => log.js} (100%) rename server/model/schema/{log.schema.js => option.js} (78%) rename server/model/schema/{tag.schema.js => tag.js} (100%) delete mode 100644 server/model/schema/user.schema.js diff --git a/package-lock.json b/package-lock.json index fadea0b..33ca7c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", + "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" + }, "binary-extensions": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", @@ -183,6 +188,11 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-shims": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", @@ -226,6 +236,11 @@ "supports-color": "2.0.0" } }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, "chokidar": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", @@ -372,6 +387,11 @@ "which": "1.3.0" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -447,6 +467,15 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "ecdsa-sig-formatter": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", + "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", + "requires": { + "base64url": "2.0.0", + "safe-buffer": "5.1.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1344,14 +1373,6 @@ } } }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, "string-width": { "version": "1.0.2", "bundled": true, @@ -1362,6 +1383,14 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, "stringstream": { "version": "0.0.5", "bundled": true, @@ -1551,6 +1580,11 @@ "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", "dev": true }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, "hooks-fixed": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz", @@ -1642,8 +1676,7 @@ "is-buffer": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", - "dev": true + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" }, "is-dotfile": { "version": "1.0.3", @@ -1754,6 +1787,11 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isemail": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", + "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1769,11 +1807,55 @@ "isarray": "1.0.0" } }, + "joi": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", + "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", + "requires": { + "hoek": "2.16.3", + "isemail": "1.2.0", + "moment": "2.18.1", + "topo": "1.1.0" + } + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "jsonwebtoken": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.1.tgz", + "integrity": "sha1-fKMk9SFfi+A5zTWmxFu4y3SkSPs=", + "requires": { + "joi": "6.10.1", + "jws": "3.1.4", + "lodash.once": "4.1.1", + "ms": "2.0.0", + "xtend": "4.0.1" + } + }, + "jwa": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", + "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", + "requires": { + "base64url": "2.0.0", + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.9", + "safe-buffer": "5.1.1" + } + }, + "jws": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", + "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", + "requires": { + "base64url": "2.0.0", + "jwa": "1.1.5", + "safe-buffer": "5.1.1" + } + }, "kareem": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz", @@ -1906,6 +1988,15 @@ "streaming-json-stringify": "3.1.0" } }, + "koa-jwt": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/koa-jwt/-/koa-jwt-3.2.2.tgz", + "integrity": "sha1-aA3mFYaWeKeVg4JwGQ16RFgeM04=", + "requires": { + "jsonwebtoken": "7.4.1", + "koa-unless": "1.0.0" + } + }, "koa-logger": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/koa-logger/-/koa-logger-2.0.1.tgz", @@ -1955,6 +2046,11 @@ } } }, + "koa-unless": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/koa-unless/-/koa-unless-1.0.0.tgz", + "integrity": "sha1-WqVzhLyIJWivyQrASFKj1YZYrus=" + }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -2058,6 +2154,11 @@ "lodash.isarray": "3.0.4" } }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.restparam": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", @@ -2095,6 +2196,16 @@ "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "dev": true }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "1.1.5" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2169,6 +2280,11 @@ } } }, + "moment": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", + "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" + }, "mongodb": { "version": "2.2.31", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.31.tgz", @@ -2809,14 +2925,6 @@ "readable-stream": "2.3.3" } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -2844,6 +2952,14 @@ } } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -2899,6 +3015,14 @@ "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", "dev": true }, + "topo": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", + "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", + "requires": { + "hoek": "2.16.3" + } + }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -3071,6 +3195,11 @@ "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", "dev": true }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", diff --git a/package.json b/package.json index a1cd516..54b81cf 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,15 @@ "koa": "^2.2.0", "koa-bodyparser": "^3.2.0", "koa-bunyan-logger": "^2.0.0", + "koa-compose": "^4.0.0", "koa-compress": "^2.0.0", "koa-json": "^2.0.2", + "koa-jwt": "^3.2.2", "koa-logger": "^2.0.1", "koa-onerror": "^1.2.1", "koa-router": "^7.1.1", "lodash": "^4.17.4", + "md5": "^2.2.1", "mongoose": "^4.11.12", "mongoose-paginate": "^5.0.3" }, diff --git a/server/app.js b/server/app.js index 3afbf02..11c8803 100644 --- a/server/app.js +++ b/server/app.js @@ -17,6 +17,8 @@ const middlewares = require('./middleware') const app = new Koa() +require('./mongo')() + // error handler onerror(app) @@ -31,20 +33,7 @@ app.use(compress()) app.use(middlewares.response) app.use(middlewares.error) -// logger -app.use(async (ctx, next) => { - const start = new Date() - await next() - const ms = new Date() - start - console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) -}) - // routes require('./routes')(app) -// error-handling -app.on('error', (err, ctx) => { - console.error('server error', err, ctx) -}); - module.exports = app diff --git a/server/config/development.js b/server/config/development.js index b9a3885..f58cf16 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -10,7 +10,7 @@ const packageInfo = require('../../package.json') module.exports = { mongo: { - uri: `mongodb://127.0.0.1/${packageInfo.name}-dev` + uri: 'mongodb://127.0.0.1/jooger-me-dev' }, auth: { cookie: { diff --git a/server/config/index.js b/server/config/index.js index 10c234d..e9c7a67 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -23,13 +23,16 @@ const baseConfig = { '10001': 'params error' }, mongo: { - useMongoClient: true + option: { + useMongoClient: true, + poolSize: 20 + } }, // TODO: Redis redis: {}, auth: { cookie: { - name: 'JOOGER_AUTH' + name: 'jooger.me' }, secretKey: `${packageInfo.name} ${packageInfo.version}`, // token过期时间 diff --git a/server/config/production.js b/server/config/production.js index e6f577e..485d047 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -10,7 +10,7 @@ const packageInfo = require('../../package.json') module.exports = { mongo: { - uri: `mongodb://127.0.0.1/${packageInfo.name}` + uri: 'mongodb://127.0.0.1/jooger-me' }, auth: { cookie: { diff --git a/server/controller/article.js b/server/controller/article.js new file mode 100644 index 0000000..32feca5 --- /dev/null +++ b/server/controller/article.js @@ -0,0 +1,37 @@ +/** + * @desc Article controller + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const { ArticleModel } = require('../model') +const ctrl = { + frontend: {}, + backend: {} +} + +ctrl.frontend.list = async (ctx, next) => { + ctx.success('123') +} + +ctrl.frontend.item = async (ctx, next) => { + ctx.success('222') +} + +ctrl.backend.list = async (ctx, next) => { + ctx.success('123') +} + +ctrl.backend.item = async (ctx, next) => { + ctx.success('222') +} + +ctrl.backend.create = async (ctx, next) => {} + +ctrl.backend.update = async (ctx, next) => {} + +ctrl.backend.delete = async (ctx, next) => {} + +module.exports = ctrl diff --git a/server/controller/article/index.js b/server/controller/article/index.js deleted file mode 100644 index 97d96fa..0000000 --- a/server/controller/article/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @desc Article controller - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - diff --git a/server/middleware/auth.js b/server/middleware/auth.js new file mode 100644 index 0000000..53af9c2 --- /dev/null +++ b/server/middleware/auth.js @@ -0,0 +1,59 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const compose = require('koa-compose') +const koajwt = require('koa-jwt') +const jwt = require('jsonwebtoken') +const config = require('../config') +const { UserModel } = require('../model') + +function verifyToken () { + return compose([ + async (ctx, next) => { + const token = ctx.cookies.get(config.auth.cookie.name, { signed: true }) + if (token) { + try { + const decodedToken = await jwt.verify(token, config.auth.secretKey) + if (decodedToken.exp > Math.floor(Date.now() / 1000)) { + // 已验证权限 + await next() + } + } catch (err) { + ctx.fail(401, err.message) + } + } + ctx.fail(401) + }, + koajwt({ + secret: config.auth.secretKey, + passthrough: true + }) + ]) +} + +exports.isAuthenticated = () => { + return compose([ + verifyToken(), + async (ctx, next) => { + if (!ctx.state.user) { + ctx.fail(401) + return + } + await next() + }, + async (ctx, next) => { + const user = await UserModel.findById(ctx.state.user._id) + if (!user) { + ctx.fail(401) + return + } + ctx.req.user = user + await next() + } + ]) +} diff --git a/server/middleware/index.js b/server/middleware/index.js index ac51561..9adf4ed 100644 --- a/server/middleware/index.js +++ b/server/middleware/index.js @@ -8,3 +8,4 @@ exports.error = require('./error') exports.response = require('./response') +exports.auth = require('./auth') diff --git a/server/middleware/response.js b/server/middleware/response.js index db4afaf..0f2b0ef 100644 --- a/server/middleware/response.js +++ b/server/middleware/response.js @@ -14,19 +14,19 @@ module.exports = async (ctx, next) => { ctx.body = { code: 200, success: true, - message: config.codeMap('200'), + message: config.codeMap['200'], data } } - ctx.fail = (code = -1, message = config.codeMap['-1'], data = null) => { + ctx.fail = (code = -1, message = '', data = null) => { ctx.status = 200 - ctx.send(200, { + ctx.body = { code, success: false, - message, + message: message || config.codeMap[code] || config.codeMap['-1'], data - }) + } } await next() diff --git a/server/model/index.js b/server/model/index.js index ce73b00..bfa2e94 100644 --- a/server/model/index.js +++ b/server/model/index.js @@ -1,8 +1,42 @@ /** - * @desc + * @desc Models entry * @author Jooger * @date 25 Sep 2017 */ 'use strict' +const mongoose = require('mongoose') +const schemas = require('./schema') +const models = {} + +Object.keys(schemas).forEach(key => { + const schema = buildSchema(schemas[key]) + if (schema) { + models[`${firstUpperCase(key)}Model`] = mongoose.model(key, schema) + } +}) + +// 构建schema +function buildSchema (schema) { + if (!schema) { + return null + } + schema.set('versionKey', false) + schema.set('toObject', { getters: true }) + schema.set('toJSON', { getters: true, virtuals: false }) + schema.pre('findOneAndUpdate', updateHook) + return schema +} + +// 更新updatedAt +function updateHook (next) { + this.findOneAndUpdate({}, { updatedAt: Date.now }) + next() +} + +function firstUpperCase (str = '') { + return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) +} + +module.exports = models diff --git a/server/model/schema/admin.js b/server/model/schema/admin.js new file mode 100644 index 0000000..adf157e --- /dev/null +++ b/server/model/schema/admin.js @@ -0,0 +1,24 @@ +/** + * @desc Admin schema + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const mongoose = require('mongoose') +const md5 = require('md5') +const config = require('../../config') + +const adminSchema = new mongoose.Schema({ + name: { type: String, default: config.auth.defaultName, required: true }, + password: { + type: String, + default: md5(`${config.auth.secretKey}${config.auth.defaultPassword}`), + required: true + }, + slogan: { type: String, default: '' }, + avatar: { type: String, default: '' } +}) + +module.exports = adminSchema diff --git a/server/model/schema/article.schema.js b/server/model/schema/article.js similarity index 97% rename from server/model/schema/article.schema.js rename to server/model/schema/article.js index 54c860a..0c1dccc 100644 --- a/server/model/schema/article.schema.js +++ b/server/model/schema/article.js @@ -24,6 +24,7 @@ const articleSchema = new mongoose.Schema({ thumb: { uid: String, title: { type: String, default: '' }, url: { type: String, default: '' }, size: Number }, // 文章状态 ( 0 草稿 | 1 已发布 ) state: { type: Number, default: 0 }, + issueNumber: { type: Number, default: 1 }, // 创建日期 createdAt: { type: Date, default: Date.now }, // 更新日期 diff --git a/server/model/schema/comment.js b/server/model/schema/comment.js new file mode 100644 index 0000000..1b46242 --- /dev/null +++ b/server/model/schema/comment.js @@ -0,0 +1,48 @@ +/** + * @desc + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate') + +const commentSchema = new mongoose.Schema({ + // 评论通用项 + createdAt: { type: Date, default: Date.now }, // 创建时间 + updatedAt: { type: Date, default: Date.now }, // 修改时间 + content: { type: String, required: true, validate: /\S+/ }, // 评论内容 + renderedContent: { type: String, required: true, validate: /\S+/ }, // marked渲染后的内容 + state: { type: Number, default: 1 }, // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 + akimetSpam: { type: Boolean, default: false }, // Akismet判定是否是垃圾评论,方便后台check + author: { // 评论发布者 + name: { type: String, required: true, validate: /\S+/ }, // 姓名 + // 邮箱 + email: { type: String, required: true, validate: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/ }, + // 个人站点地址 + site: { type: String, validate: /^((https|http):\/\/)+[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/ } + }, + ups: { type: Number, default: 0 }, // 点赞数 + sticky: { type: Boolean, default: false }, // 是否置顶 + type: { type: Number, default: 0 }, // 类型 0 文章评论 | 1 页面评论 包括留言板或者作品展示页面等 + meta: { + ip: String, // 用户IP + location: Object, // IP所在地 + agent: { type: String, validate: /\S+/ }, // user agent + referer: { type: String, default: '' } + } , + extends: [{ + key: { type: String, validate: /\S+/ }, + value: { type: String, validate: /\S+/ } + }], + pageId: String, // 页面id,type = 0时是文章的ID,type = 1时是页面的name,option model中Menu的name + // 子评论具备项 + parent: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, // 父评论 parent和forward二者必须同时存在 + forward: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' } // 前一条评论ID,可以是parent_id, 比如 B评论 是 A评论的回复,则B.forward_id = A._id,主要是为了查看评论对话时的评论树构建 +}) + +commentSchema.plugin(mongoosePaginate) + +module.exports = commentSchema diff --git a/server/model/schema/index.js b/server/model/schema/index.js new file mode 100644 index 0000000..916bf80 --- /dev/null +++ b/server/model/schema/index.js @@ -0,0 +1,14 @@ +/** + * @desc Schemas entry + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +exports.article = require('./article') +exports.comment = require('./comment') +exports.tag = require('./tag') +exports.admin = require('./admin') +// exports.log = require('./log') +// exports.option = require('./option') diff --git a/server/model/schema/comment.schema.js b/server/model/schema/log.js similarity index 100% rename from server/model/schema/comment.schema.js rename to server/model/schema/log.js diff --git a/server/model/schema/log.schema.js b/server/model/schema/option.js similarity index 78% rename from server/model/schema/log.schema.js rename to server/model/schema/option.js index ce73b00..1cbb602 100644 --- a/server/model/schema/log.schema.js +++ b/server/model/schema/option.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc Option schema * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/model/schema/tag.schema.js b/server/model/schema/tag.js similarity index 100% rename from server/model/schema/tag.schema.js rename to server/model/schema/tag.js diff --git a/server/model/schema/user.schema.js b/server/model/schema/user.schema.js deleted file mode 100644 index ce73b00..0000000 --- a/server/model/schema/user.schema.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @desc - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - diff --git a/server/mongo.js b/server/mongo.js index e67a978..7ee39f8 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -7,7 +7,14 @@ 'use strict' const mongoose = require('mongoose') -const config = require('./config/env') - -mongoose.Promise = global.Promise +const config = require('./config') +module.exports = function () { + mongoose.Promise = global.Promise + mongoose.connect(config.mongo.uri, config.mongo.option, err => { + if (err) { + console.error('connect to %s error: ', config.mongo.uri, err.message) + process.exit(0) + } + }) +} diff --git a/server/routes/backend.js b/server/routes/backend.js index ce73b00..2018096 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -6,3 +6,11 @@ 'use strict' +const router = require('koa-router')() +const articleCtrl = require('../controller/article') +const { auth } = require('../middleware') + +router.get('/articles', auth.isAuthenticated(), articleCtrl.backend.list) +router.get('/articles/:id', auth.isAuthenticated(), articleCtrl.backend.item) + +module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js index ce73b00..fdae829 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -6,3 +6,10 @@ 'use strict' +const router = require('koa-router')() +const articleCtrl = require('../controller/article') + +router.get('/articles', articleCtrl.frontend.list) +router.get('/articles/:id', articleCtrl.frontend.item) + +module.exports = router diff --git a/server/routes/index.js b/server/routes/index.js index 4b6d066..d414011 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -6,15 +6,19 @@ 'use strict' -const Router = require('koa-router') -const frontend = require('./frontend') -const backend = require('./backend') -const router = new Router({ +const router = require('koa-router')({ prefix: '/api' }) +const frontend = require('./frontend') +const backend = require('./backend') module.exports = app => { - // router.use('/frontend', frontend.routes(), frontend.allowedMethods()) - // router.use('/backend', backend.routes(), backend.allowedMethods()) + router.use('/frontend', frontend.routes(), frontend.allowedMethods()) + router.use('/backend', backend.routes(), backend.allowedMethods()) + router.all('*', (ctx,next)=> { + ctx.fail(404, `${ctx.path} 不支持 ${ctx.method} 请求类型`) + ctx.status = 404 + }) + app.use(router.routes(), router.allowedMethods()) } From 83084d857f165e761842a7cc4b6b052e5bf7ed17 Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 25 Sep 2017 23:52:10 +0800 Subject: [PATCH 003/208] [update] update controller --- package-lock.json | 39 +++++++++++++++++++ package.json | 1 + server/app.js | 5 ++- server/config/index.js | 1 + server/controller/article.js | 73 ++++++++++++++++++++++++++++++++++- server/middleware/auth.js | 3 +- server/middleware/error.js | 4 +- server/middleware/response.js | 4 +- server/model/index.js | 5 +-- server/util/index.js | 14 +++++++ server/util/validation.js | 19 +++++++++ 11 files changed, 156 insertions(+), 12 deletions(-) create mode 100644 server/util/index.js create mode 100644 server/util/validation.js diff --git a/package-lock.json b/package-lock.json index 33ca7c0..6fb7799 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,6 +100,14 @@ "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "binary-extensions": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", @@ -213,6 +221,11 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", @@ -1914,6 +1927,17 @@ "co-body": "4.2.0" } }, + "koa-bouncer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/koa-bouncer/-/koa-bouncer-6.0.0.tgz", + "integrity": "sha1-XV9WJzYnU1nzpaR19DVbTxqxto0=", + "requires": { + "better-assert": "1.0.2", + "debug": "2.6.9", + "lodash": "4.17.4", + "validator": "4.9.0" + } + }, "koa-bunyan-logger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/koa-bunyan-logger/-/koa-bunyan-logger-2.0.0.tgz", @@ -3118,6 +3142,21 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" }, + "validator": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-4.9.0.tgz", + "integrity": "sha1-CC/84qdhSP8HqOienCukOq8S7Ew=", + "requires": { + "depd": "1.1.0" + }, + "dependencies": { + "depd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" + } + } + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 54b81cf..a01067f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "debug": "^2.6.9", "koa": "^2.2.0", "koa-bodyparser": "^3.2.0", + "koa-bouncer": "^6.0.0", "koa-bunyan-logger": "^2.0.0", "koa-compose": "^4.0.0", "koa-compress": "^2.0.0", diff --git a/server/app.js b/server/app.js index 11c8803..506a145 100644 --- a/server/app.js +++ b/server/app.js @@ -9,14 +9,16 @@ const Koa = require('koa') const json = require('koa-json') const logger = require('koa-logger') -const compress = require('koa-compress') const onerror = require('koa-onerror') +const bouncer = require('koa-bouncer') +const compress = require('koa-compress') const bodyparser = require('koa-bodyparser') const koaBunyanLogger = require('koa-bunyan-logger') const middlewares = require('./middleware') const app = new Koa() +require('./util') require('./mongo')() // error handler @@ -30,6 +32,7 @@ app.use(json()) app.use(logger()) app.use(koaBunyanLogger()) app.use(compress()) +app.use(bouncer.middleware()) app.use(middlewares.response) app.use(middlewares.error) diff --git a/server/config/index.js b/server/config/index.js index e9c7a67..cd745fd 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -14,6 +14,7 @@ const baseConfig = { env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3000, + pageSize: 12, codeMap: { '-1': 'fail', '200': 'success', diff --git a/server/controller/article.js b/server/controller/article.js index 32feca5..94ad866 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -6,14 +6,83 @@ 'use strict' -const { ArticleModel } = require('../model') +const config = require('../config') +const { ArticleModel, TagModel } = require('../model') const ctrl = { frontend: {}, backend: {} } ctrl.frontend.list = async (ctx, next) => { - ctx.success('123') + const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the per_page parameter should be greater than 0').val() + const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'the page parameter should be greater than 0').val() + const state = ctx.validateQuery('state').defaultTo(1).optional().toInt().isIn([0, 1], 'the expected value of the article state is 0 or 1').val() + const tag = ctx.validateQuery('tag').defaultTo('').toString().val() + const keyword = ctx.validateQuery('keyword').toString().defaultTo().val() + + // 过滤条件 + const options = { + sort: { createdAt: -1 }, + page, + limit: pageSize, + populate: [ + { + path: 'tag', + select: 'name description', + match: { + forbidden: 0 + } + } + ] + } + + const query = { + state + } + + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { title: keywordReg }, + { description: keywordReg } + ] + } + + // 标签 + if (tag) { + // 如果是id + if (isObjectId(tag)) { + query.tag = tag + } else { + // 普通字符串,需要先查到id + await TagModel.findOne({ name: tag }).exec() + .then(t => { + query.tag = t && t._id || createObjectId() + }) + .catch(() => { + logger.error('标签查找失败') + query.tag = createObjectId() + }) + } + } + + const articles = await ArticleModel.paginate(query, options).catch(err => { + ctx.log.error(err.message) + ctx.fail(-1, '文章列表获取失败') + }) + + if (articles) { + ctx.success({ + list: articles.docs, + pagination: { + total: articles.total, + current_page: articles.page > articles.pages ? articles.pages : articles.page, + total_page: articles.pages, + per_page: articles.limit + } + }) + } } ctrl.frontend.item = async (ctx, next) => { diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 53af9c2..2a8b189 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -52,7 +52,8 @@ exports.isAuthenticated = () => { ctx.fail(401) return } - ctx.req.user = user + ctx._user = user + ctx._isAuthenticated = true await next() } ]) diff --git a/server/middleware/error.js b/server/middleware/error.js index e1b7e66..4a6d725 100644 --- a/server/middleware/error.js +++ b/server/middleware/error.js @@ -11,9 +11,9 @@ module.exports = async (ctx, next) => { await next() } catch (err) { const code = err.status || 500 - ctx.fail(code) + ctx.fail(code, err.message) ctx.status = code - + if (code === 500) { ctx.log.error( { req: ctx.req, err }, diff --git a/server/middleware/response.js b/server/middleware/response.js index 0f2b0ef..8f31bef 100644 --- a/server/middleware/response.js +++ b/server/middleware/response.js @@ -9,12 +9,12 @@ const config = require('../config') module.exports = async (ctx, next) => { - ctx.success = (data = null) => { + ctx.success = (data = null, message = config.codeMap[200]) => { ctx.status = 200 ctx.body = { code: 200, success: true, - message: config.codeMap['200'], + message, data } } diff --git a/server/model/index.js b/server/model/index.js index bfa2e94..2f18970 100644 --- a/server/model/index.js +++ b/server/model/index.js @@ -8,6 +8,7 @@ const mongoose = require('mongoose') const schemas = require('./schema') +const { firstUpperCase } = require('../util') const models = {} Object.keys(schemas).forEach(key => { @@ -35,8 +36,4 @@ function updateHook (next) { next() } -function firstUpperCase (str = '') { - return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) -} - module.exports = models diff --git a/server/util/index.js b/server/util/index.js new file mode 100644 index 0000000..53c170c --- /dev/null +++ b/server/util/index.js @@ -0,0 +1,14 @@ +/** + * @desc Util entry + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +exports.validation = require('./validation') + +exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) + +// 首字母大写 +exports.firstUpperCase = (str = '') => str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) diff --git a/server/util/validation.js b/server/util/validation.js new file mode 100644 index 0000000..735ecba --- /dev/null +++ b/server/util/validation.js @@ -0,0 +1,19 @@ +/** + * @desc Custom Validations for koa-bouncer + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const mongoose = require('mongoose') +const Validator = require('koa-bouncer').Validator + +Validator.addMethod('isObjectId', function (val, tip) { + if (val && !mongoose.Types.ObjectId.isValid(val)) { + this.throwError(tip || `the ${this.key} do not match the ObjectId type`) + } + return this +}) + +module.exports = Validator From 6d8e8a317a9ac1516fb38c71d9bb7099b78caf32 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 10:17:48 +0800 Subject: [PATCH 004/208] [fix] fix params validate bug --- server/config/index.js | 2 +- server/controller/article.js | 26 +++++++------------------- server/middleware/error.js | 10 ++++++++-- server/routes/backend.js | 4 ++-- server/routes/frontend.js | 4 ++-- server/routes/index.js | 1 + 6 files changed, 21 insertions(+), 26 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index cd745fd..76d0dc8 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -18,7 +18,7 @@ const baseConfig = { codeMap: { '-1': 'fail', '200': 'success', - '401': 'token expired', + '401': 'authentication failure', '403': 'forbidden', '500': 'server error', '10001': 'params error' diff --git a/server/controller/article.js b/server/controller/article.js index 94ad866..6d0db2d 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -9,14 +9,12 @@ const config = require('../config') const { ArticleModel, TagModel } = require('../model') const ctrl = { - frontend: {}, - backend: {} } -ctrl.frontend.list = async (ctx, next) => { +ctrl.list = async (ctx, next) => { const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the per_page parameter should be greater than 0').val() const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'the page parameter should be greater than 0').val() - const state = ctx.validateQuery('state').defaultTo(1).optional().toInt().isIn([0, 1], 'the expected value of the article state is 0 or 1').val() + const state = ctx.validateQuery('state').defaultTo(1).optional().toInt().isIn([0, 1], 'the state parameter is not the expected value').val() const tag = ctx.validateQuery('tag').defaultTo('').toString().val() const keyword = ctx.validateQuery('keyword').toString().defaultTo().val() @@ -36,9 +34,7 @@ ctrl.frontend.list = async (ctx, next) => { ] } - const query = { - state - } + const query = { state } // 搜索关键词 if (keyword) { @@ -85,22 +81,14 @@ ctrl.frontend.list = async (ctx, next) => { } } -ctrl.frontend.item = async (ctx, next) => { - ctx.success('222') -} - -ctrl.backend.list = async (ctx, next) => { - ctx.success('123') -} - -ctrl.backend.item = async (ctx, next) => { +ctrl.item = async (ctx, next) => { ctx.success('222') } -ctrl.backend.create = async (ctx, next) => {} +ctrl.create = async (ctx, next) => {} -ctrl.backend.update = async (ctx, next) => {} +ctrl.update = async (ctx, next) => {} -ctrl.backend.delete = async (ctx, next) => {} +ctrl.delete = async (ctx, next) => {} module.exports = ctrl diff --git a/server/middleware/error.js b/server/middleware/error.js index 4a6d725..7ffc239 100644 --- a/server/middleware/error.js +++ b/server/middleware/error.js @@ -10,9 +10,15 @@ module.exports = async (ctx, next) => { try { await next() } catch (err) { - const code = err.status || 500 + console.log(err.name) + let code = err.status || 500 + + if (err.name === 'ValidationError') { + code = 10001 + } + ctx.fail(code, err.message) - ctx.status = code + ctx.status = 200 if (code === 500) { ctx.log.error( diff --git a/server/routes/backend.js b/server/routes/backend.js index 2018096..2f38e9a 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -10,7 +10,7 @@ const router = require('koa-router')() const articleCtrl = require('../controller/article') const { auth } = require('../middleware') -router.get('/articles', auth.isAuthenticated(), articleCtrl.backend.list) -router.get('/articles/:id', auth.isAuthenticated(), articleCtrl.backend.item) +router.get('/articles', auth.isAuthenticated(), articleCtrl.list) +router.get('/articles/:id', auth.isAuthenticated(), articleCtrl.item) module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js index fdae829..453baae 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -9,7 +9,7 @@ const router = require('koa-router')() const articleCtrl = require('../controller/article') -router.get('/articles', articleCtrl.frontend.list) -router.get('/articles/:id', articleCtrl.frontend.item) +router.get('/articles', articleCtrl.list) +router.get('/articles/:id', articleCtrl.item) module.exports = router diff --git a/server/routes/index.js b/server/routes/index.js index d414011..779c7ea 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -15,6 +15,7 @@ const backend = require('./backend') module.exports = app => { router.use('/frontend', frontend.routes(), frontend.allowedMethods()) router.use('/backend', backend.routes(), backend.allowedMethods()) + router.all('*', (ctx,next)=> { ctx.fail(404, `${ctx.path} 不支持 ${ctx.method} 请求类型`) ctx.status = 404 From 28b18c2bdc3de5bab9c60c6f9e23b28ff8160d27 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 12:14:58 +0800 Subject: [PATCH 005/208] [add] add api of creating and updating article --- package-lock.json | 10 +++ package.json | 2 + server/app.js | 3 +- server/config/development.js | 3 +- server/controller/article.js | 137 ++++++++++++++++++++++++++++++--- server/middleware/auth.js | 23 +++++- server/middleware/error.js | 1 - server/model/index.js | 8 +- server/model/schema/article.js | 5 +- server/routes/backend.js | 4 + server/routes/frontend.js | 1 + server/routes/index.js | 2 +- server/util/index.js | 4 +- server/util/marked.js | 121 +++++++++++++++++++++++++++++ server/util/validation.js | 19 ----- server/validation.js | 45 +++++++++++ 16 files changed, 348 insertions(+), 40 deletions(-) create mode 100644 server/util/marked.js delete mode 100644 server/util/validation.js create mode 100644 server/validation.js diff --git a/package-lock.json b/package-lock.json index 6fb7799..2c3f744 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1593,6 +1593,11 @@ "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", "dev": true }, + "highlight.js": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", + "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=" + }, "hoek": { "version": "2.16.3", "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", @@ -2220,6 +2225,11 @@ "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "dev": true }, + "marked": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", + "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=" + }, "md5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", diff --git a/package.json b/package.json index a01067f..122d966 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "debug": "^2.6.9", + "highlight.js": "^9.12.0", "koa": "^2.2.0", "koa-bodyparser": "^3.2.0", "koa-bouncer": "^6.0.0", @@ -22,6 +23,7 @@ "koa-onerror": "^1.2.1", "koa-router": "^7.1.1", "lodash": "^4.17.4", + "marked": "^0.3.6", "md5": "^2.2.1", "mongoose": "^4.11.12", "mongoose-paginate": "^5.0.3" diff --git a/server/app.js b/server/app.js index 506a145..a721394 100644 --- a/server/app.js +++ b/server/app.js @@ -18,9 +18,10 @@ const middlewares = require('./middleware') const app = new Koa() -require('./util') require('./mongo')() +bouncer.Validator = require('./validation') + // error handler onerror(app) diff --git a/server/config/development.js b/server/config/development.js index f58cf16..8a4adb4 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -10,7 +10,8 @@ const packageInfo = require('../../package.json') module.exports = { mongo: { - uri: 'mongodb://127.0.0.1/jooger-me-dev' + // uri: 'mongodb://127.0.0.1/jooger-me-dev' + uri: 'mongodb://127.0.0.1/koapi' }, auth: { cookie: { diff --git a/server/controller/article.js b/server/controller/article.js index 6d0db2d..4c7a38b 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -8,14 +8,15 @@ const config = require('../config') const { ArticleModel, TagModel } = require('../model') +const { marked } = require('../util') const ctrl = { } ctrl.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the per_page parameter should be greater than 0').val() - const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'the page parameter should be greater than 0').val() - const state = ctx.validateQuery('state').defaultTo(1).optional().toInt().isIn([0, 1], 'the state parameter is not the expected value').val() - const tag = ctx.validateQuery('tag').defaultTo('').toString().val() + const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the "per_page" parameter should be greater than 0').val() + const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'the "page" parameter should be greater than 0').val() + const state = ctx.validateQuery('state').defaultTo(1).optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() + const tag = ctx.validateQuery('tag').isObjectId().val() const keyword = ctx.validateQuery('keyword').toString().defaultTo().val() // 过滤条件 @@ -57,15 +58,22 @@ ctrl.list = async (ctx, next) => { query.tag = t && t._id || createObjectId() }) .catch(() => { - logger.error('标签查找失败') query.tag = createObjectId() }) } } + // 未通过权限校验(前台获取文章列表) + if (!ctx._isAuthenticated) { + // 将文章状态重置为1 + query.state = 1 + // 文章列表不需要content和state + options.select = '-content -renderedContent -state' + } + const articles = await ArticleModel.paginate(query, options).catch(err => { ctx.log.error(err.message) - ctx.fail(-1, '文章列表获取失败') + ctx.fail() }) if (articles) { @@ -78,17 +86,128 @@ ctrl.list = async (ctx, next) => { per_page: articles.limit } }) + } else { + ctx.fail(-1, 'the article list access failed') } } ctrl.item = async (ctx, next) => { - ctx.success('222') + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + + let data = null + let queryPs = null + // 只有前台博客访问文章的时候pv才+1 + if (!ctx._isAuthenticated) { + queryPs = ArticleModel.findByIdAndUpdate(id, { $inc: { 'meta.pvs': 1 } }, { new: true }).select('-content') + } else { + queryPs = ArticleModel.findById(id) + } + + data = await queryPs.populate('tag').exec().catch(err => { + ctx.log.error(err.message) + ctx.fail() + }) + + if (data) { + data = data.toObject() + await getRelatedArticles(ctx, data) + await getSiblingArticles(ctx, data) + ctx.success(data) + } else { + ctx.fail(-1, 'the article not found') + } + } -ctrl.create = async (ctx, next) => {} +ctrl.create = async (ctx, next) => { + const title = ctx.validateBody('title').required('the title parameter is required').notEmpty().val() + const content = ctx.validateBody('content').required('the content parameter is required').notEmpty().val() + const keywords = ctx.validateBody('keywords').defaultTo([]).isArray('the keywords parameter should be Array type').val() + const description = ctx.validateBody('description').optional().isString().val() + const data = await new ArticleModel({ + title, + content, + renderedContent: marked(content), + keywords, + description + }).save().catch(err => { + ctx.log.error(err.message) + ctx.fail() + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} -ctrl.update = async (ctx, next) => {} +ctrl.update = async (ctx, next) => { + const title = ctx.validateBody('title').required('the title parameter is required').notEmpty().val() + const content = ctx.validateBody('content').required('the content parameter is required').notEmpty().val() + const keywords = ctx.validateBody('keywords').defaultTo([]).isArray('the keywords parameter should be Array type').val() + const description = ctx.validateBody('description').optional().isString().val() + const tag = ctx.validateBody('tag').defaultTo([]).isObjectIdArray().val() + console.log(tag) +} ctrl.delete = async (ctx, next) => {} +ctrl.like = async (ctx, next) => {} + +/** + * 获取相关文章 + * @param {} ctx koa ctx + * @param {} data 文章数据 + */ +async function getRelatedArticles (ctx, data) { + data.related = [] + if (data && data.tag && data.tag.length) { + const articles = await ArticleModel.find({ _id: { $nin: [ data._id ] }, state: 1, tag: { $in: data.tag.map(t => t._id) }}) + .select('title thumb createdAt meta') + .exec() + .catch(err => { + ctx.log.error('related articles access failed, err: ', err.message) + }) + + if (articles) { + data.related = articles + } + } +} + +/** + * 获取相邻的文章 + * @param {} ctx koa ctx + * @param {} data 文章数据 + */ +async function getSiblingArticles (ctx, data) { + if (data && data._id) { + const query = {} + // 如果未通过权限校验,将文章状态重置为1 + if (!ctx._isAuthenticated) { + query.state = 1 + } + let prev = await ArticleModel.findOne(query) + .select('title createdAt thumb') + .sort('-createdAt') + .lt('createdAt', data.createdAt) + .exec() + .catch(err => { + ctx.log.error('adjacent articles access failed, err: ', err.message) + }) + let next = await ArticleModel.findOne(query) + .select('title createdAt thumb') + .sort('createdAt') + .gt('createdAt', data.createdAt) + .exec() + .catch(err => { + ctx.log.error('adjacent articles access failed, err: ', err.message) + }) + prev = prev && prev.toObject() + next = next && next.toObject() + data.adjacent = { prev, next } + } +} + module.exports = ctrl diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 2a8b189..14abf2b 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -11,10 +11,24 @@ const koajwt = require('koa-jwt') const jwt = require('jsonwebtoken') const config = require('../config') const { UserModel } = require('../model') +const isProd = process.env.NODE_ENV === 'production' + +// 开发环境下,请求携带_DEV_参数,视为已验证 +function devAuth () { + return async (ctx, next) => { + if (!isProd && ctx.query._DEV_) { + ctx._devauth_ = true + await next() + } + } +} function verifyToken () { return compose([ async (ctx, next) => { + if (ctx._devauth_) { + return await next() + } const token = ctx.cookies.get(config.auth.cookie.name, { signed: true }) if (token) { try { @@ -38,15 +52,22 @@ function verifyToken () { exports.isAuthenticated = () => { return compose([ + devAuth(), verifyToken(), async (ctx, next) => { - if (!ctx.state.user) { + if (ctx._devauth_) { + return await next() + } else if (!ctx.state.user) { ctx.fail(401) return } await next() }, async (ctx, next) => { + if (ctx._devauth_) { + ctx._isAuthenticated = true + return await next() + } const user = await UserModel.findById(ctx.state.user._id) if (!user) { ctx.fail(401) diff --git a/server/middleware/error.js b/server/middleware/error.js index 7ffc239..4d87123 100644 --- a/server/middleware/error.js +++ b/server/middleware/error.js @@ -10,7 +10,6 @@ module.exports = async (ctx, next) => { try { await next() } catch (err) { - console.log(err.name) let code = err.status || 500 if (err.name === 'ValidationError') { diff --git a/server/model/index.js b/server/model/index.js index 2f18970..224a844 100644 --- a/server/model/index.js +++ b/server/model/index.js @@ -12,14 +12,14 @@ const { firstUpperCase } = require('../util') const models = {} Object.keys(schemas).forEach(key => { - const schema = buildSchema(schemas[key]) + const schema = getSchema(schemas[key]) if (schema) { - models[`${firstUpperCase(key)}Model`] = mongoose.model(key, schema) + models[`${firstUpperCase(key)}Model`] = mongoose.model(firstUpperCase(key), schema) } }) // 构建schema -function buildSchema (schema) { +function getSchema (schema) { if (!schema) { return null } @@ -32,7 +32,7 @@ function buildSchema (schema) { // 更新updatedAt function updateHook (next) { - this.findOneAndUpdate({}, { updatedAt: Date.now }) + this.findOneAndUpdate({}, { updatedAt: Date.now() }) next() } diff --git a/server/model/schema/article.js b/server/model/schema/article.js index 0c1dccc..f4dcc5b 100644 --- a/server/model/schema/article.js +++ b/server/model/schema/article.js @@ -20,10 +20,13 @@ const articleSchema = new mongoose.Schema({ content: { type: String, required: true, validate: /\S+/ }, // markdown渲染后的htmln内容 renderedContent: { type: String, required: true, validate: /\S+/ }, + // 标签 + tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) thumb: { uid: String, title: { type: String, default: '' }, url: { type: String, default: '' }, size: Number }, // 文章状态 ( 0 草稿 | 1 已发布 ) state: { type: Number, default: 0 }, + // github issue issueNumber: { type: Number, default: 1 }, // 创建日期 createdAt: { type: Date, default: Date.now }, @@ -31,8 +34,6 @@ const articleSchema = new mongoose.Schema({ updatedAt: { type: Date, default: Date.now }, // 发布日期 publishedAt: { type: Date, default: Date.now }, - // 标签 - tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], // 文章元数据 (浏览量, 喜欢数, 评论数) meta: { pvs: { type: Number, default: 0 }, diff --git a/server/routes/backend.js b/server/routes/backend.js index 2f38e9a..fbf0117 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -12,5 +12,9 @@ const { auth } = require('../middleware') router.get('/articles', auth.isAuthenticated(), articleCtrl.list) router.get('/articles/:id', auth.isAuthenticated(), articleCtrl.item) +router.post('/articles', auth.isAuthenticated(), articleCtrl.create) +router.patch('/articles/:id', auth.isAuthenticated(), articleCtrl.update) +router.delete('/articles/:id', auth.isAuthenticated(), articleCtrl.delete) +router.get('/articles/:id/like', auth.isAuthenticated(), articleCtrl.like) module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 453baae..074d918 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -11,5 +11,6 @@ const articleCtrl = require('../controller/article') router.get('/articles', articleCtrl.list) router.get('/articles/:id', articleCtrl.item) +router.get('/articles/:id/like', articleCtrl.like) module.exports = router diff --git a/server/routes/index.js b/server/routes/index.js index 779c7ea..749f63b 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -13,7 +13,7 @@ const frontend = require('./frontend') const backend = require('./backend') module.exports = app => { - router.use('/frontend', frontend.routes(), frontend.allowedMethods()) + router.use(frontend.routes(), frontend.allowedMethods()) router.use('/backend', backend.routes(), backend.allowedMethods()) router.all('*', (ctx,next)=> { diff --git a/server/util/index.js b/server/util/index.js index 53c170c..5866e64 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -6,7 +6,9 @@ 'use strict' -exports.validation = require('./validation') +const mongoose = require('mongoose') + +exports.marked = require('./marked') exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) diff --git a/server/util/marked.js b/server/util/marked.js new file mode 100644 index 0000000..0cbad57 --- /dev/null +++ b/server/util/marked.js @@ -0,0 +1,121 @@ +/** + * @desc Markdown renderer + * @author Jooger + * @date 26 Sep 2017 + */ + +const marked = require('marked') +const highlight = require('highlight.js') + +const languages = ['xml', 'bash', 'css', 'markdown', 'http', 'java', 'javascript', 'json', 'makefile', 'nginx', 'python', 'scss', 'sql', 'stylus'] +highlight.registerLanguage('xml', require('highlight.js/lib/languages/xml')) +highlight.registerLanguage('bash', require('highlight.js/lib/languages/bash')) +highlight.registerLanguage('css', require('highlight.js/lib/languages/css')) +highlight.registerLanguage('markdown', require('highlight.js/lib/languages/markdown')) +highlight.registerLanguage('http', require('highlight.js/lib/languages/http')) +highlight.registerLanguage('java', require('highlight.js/lib/languages/java')) +highlight.registerLanguage('javascript', require('highlight.js/lib/languages/javascript')) +highlight.registerLanguage('json', require('highlight.js/lib/languages/json')) +highlight.registerLanguage('makefile', require('highlight.js/lib/languages/makefile')) +highlight.registerLanguage('nginx', require('highlight.js/lib/languages/nginx')) +highlight.registerLanguage('python', require('highlight.js/lib/languages/python')) +highlight.registerLanguage('scss', require('highlight.js/lib/languages/scss')) +highlight.registerLanguage('sql', require('highlight.js/lib/languages/sql')) +highlight.registerLanguage('stylus', require('highlight.js/lib/languages/stylus')) +highlight.configure({ + classPrefix: '' // don't append class prefix +}) + +const renderer = new marked.Renderer() + +renderer.heading = function (text, level) { + return `${text}` +} + +renderer.link = function (href, title, text) { + const isOrigin = href.indexOf('jooger.me') > -1 + const isImage = /(/gi.test(text) + return ` + ${text} + `.replace(/\s+/g, ' ').replace('\n', '') +} + +renderer.image = function (href, title, text) { + return ` + ${text || title || href} + `.replace(/\s+/g, ' ').replace('\n', '') +} + +renderer.code = function (code, lang, escaped) { + if (this.options.highlight) { + var out = this.options.highlight(code, lang) + if (out != null && out !== code) { + escaped = true + code = out + } + } + + const lineCode = code.split('\n') + const codeWrapper = lineCode.map((line, index) => `${line}${index !== lineCode.length - 1 ? '
' : ''}`.replace(/\s+/g, ' ')).join('') + + if (!lang) { + return '
' +
+    codeWrapper +
+      '\n
' + } + + return '
' + '' +
+    codeWrapper +
+    '\n
\n' +} + +marked.setOptions({ + renderer, + gfm: true, + pedantic: false, + sanitize: false, + tables: true, + breaks: true, + smartLists: true, + smartypants: true, + highlight (code, lang) { + if (!~languages.indexOf(lang)) { + return highlight.highlightAuto(code).value + } + return highlight.highlight(lang, code).value + } +}) + +// 生成文章中的title id +function generateId (len) { + const chars = `ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz` + len = len | 8 + let id = '' + for (let i = 0; i < len; i++) { + id += chars[Math.floor(Math.random() * chars.length)] + } + return id +} + +function escape (html, encode) { + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +module.exports = marked diff --git a/server/util/validation.js b/server/util/validation.js deleted file mode 100644 index 735ecba..0000000 --- a/server/util/validation.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @desc Custom Validations for koa-bouncer - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const mongoose = require('mongoose') -const Validator = require('koa-bouncer').Validator - -Validator.addMethod('isObjectId', function (val, tip) { - if (val && !mongoose.Types.ObjectId.isValid(val)) { - this.throwError(tip || `the ${this.key} do not match the ObjectId type`) - } - return this -}) - -module.exports = Validator diff --git a/server/validation.js b/server/validation.js new file mode 100644 index 0000000..d766c44 --- /dev/null +++ b/server/validation.js @@ -0,0 +1,45 @@ +/** + * @desc Custom Validations for koa-bouncer + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const mongoose = require('mongoose') +const Validator = require('koa-bouncer').Validator +const { isObjectId } = require('./util') + +Validator.addMethod('notEmpty', function (tip) { + this.isString('the "${this.key}" parameter should be String type') + if (this.val().length === 0) { + this.throwError(tip || `the "${this.key}" parameter should not be empty value`) + } + return this +}) + +Validator.addMethod('isObjectId', function (tip) { + const val = this.val() + if (val !== undefined) { + this.toString() + if (!mongoose.Types.ObjectId.isValid(val)) { + this.throwError(tip || `the "${this.key}" parameter should be ObjectId type`) + } + } + return this +}) + +Validator.addMethod('isObjectIdArray', function (tip) { + const val = this.val() + if (val !== undefined) { + this.isArray() + val.every(data => { + if (!isObjectId(data)) { + this.throwError(tip || `the "${this.key}" parameter contains a data(${data}) that is not ObjectId type`) + } + }) + } + return this +}) + +module.exports = Validator From 4d02cb7a39a40ae5a1695244c5df812d486736f3 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 15:31:43 +0800 Subject: [PATCH 006/208] [add] add api of deleting and liking article --- server/config/development.js | 3 +- server/controller/article.js | 103 ++++++++++++++++++++++++++++----- server/middleware/auth.js | 2 +- server/model/schema/article.js | 2 +- server/validation.js | 2 +- 5 files changed, 91 insertions(+), 21 deletions(-) diff --git a/server/config/development.js b/server/config/development.js index 8a4adb4..f58cf16 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -10,8 +10,7 @@ const packageInfo = require('../../package.json') module.exports = { mongo: { - // uri: 'mongodb://127.0.0.1/jooger-me-dev' - uri: 'mongodb://127.0.0.1/koapi' + uri: 'mongodb://127.0.0.1/jooger-me-dev' }, auth: { cookie: { diff --git a/server/controller/article.js b/server/controller/article.js index 4c7a38b..e4275d8 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -8,16 +8,16 @@ const config = require('../config') const { ArticleModel, TagModel } = require('../model') -const { marked } = require('../util') +const { marked, isObjectId } = require('../util') const ctrl = { } ctrl.list = async (ctx, next) => { const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the "per_page" parameter should be greater than 0').val() const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'the "page" parameter should be greater than 0').val() - const state = ctx.validateQuery('state').defaultTo(1).optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() - const tag = ctx.validateQuery('tag').isObjectId().val() - const keyword = ctx.validateQuery('keyword').toString().defaultTo().val() + const state = ctx.validateQuery('state').defaultTo(1).toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() + const tag = ctx.validateQuery('tag').optional().toString().val() + const keyword = ctx.validateQuery('keyword').optional().toString().val() // 过滤条件 const options = { @@ -35,6 +35,7 @@ ctrl.list = async (ctx, next) => { ] } + // 查询条件 const query = { state } // 搜索关键词 @@ -120,10 +121,21 @@ ctrl.item = async (ctx, next) => { } ctrl.create = async (ctx, next) => { - const title = ctx.validateBody('title').required('the title parameter is required').notEmpty().val() - const content = ctx.validateBody('content').required('the content parameter is required').notEmpty().val() - const keywords = ctx.validateBody('keywords').defaultTo([]).isArray('the keywords parameter should be Array type').val() - const description = ctx.validateBody('description').optional().isString().val() + const title = ctx.validateBody('title') + .required('the "title" parameter is required') + .notEmpty() + .isString('the "title" parameter should be String type') + .val() + const content = ctx.validateBody('content') + .required('the "content" parameter is required') + .notEmpty() + .isString('the "content" parameter should be String type') + .val() + const keywords = ctx.validateBody('keywords').optional().defaultTo([]).isArray('the "keywords" parameter should be Array type').val() + const description = ctx.validateBody('description') + .optional() + .isString('the "description" parameter should be String type') + .val() const data = await new ArticleModel({ title, content, @@ -143,17 +155,76 @@ ctrl.create = async (ctx, next) => { } ctrl.update = async (ctx, next) => { - const title = ctx.validateBody('title').required('the title parameter is required').notEmpty().val() - const content = ctx.validateBody('content').required('the content parameter is required').notEmpty().val() - const keywords = ctx.validateBody('keywords').defaultTo([]).isArray('the keywords parameter should be Array type').val() - const description = ctx.validateBody('description').optional().isString().val() - const tag = ctx.validateBody('tag').defaultTo([]).isObjectIdArray().val() - console.log(tag) + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const title = ctx.validateBody('title').optional().isString('the "title" parameter should be String type').val() + const content = ctx.validateBody('content').optional().isString('the "content" parameter should be String type').val() + const keywords = ctx.validateBody('keywords').optional().isArray('the "keywords" parameter should be Array type').val() + const description = ctx.validateBody('description').optional().isString('the "description" parameter should be String type').val() + const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() + const thumb = ctx.validateBody('thumb').optional().isString('the "thumb" parameter should be String type').val() + const issueNumber = ctx.validateBody('state').optional().toInt().gte(1, 'the "state" parameter must be 1 or older').val() + const article = {} + + title && (article.title = title) + keywords && (article.keywords = keywords) + description && (article.description = description) + tag && (article.tag = tag) + state && (article.state = state) + thumb && (article.thumb = thumb) + issueNumber && (article.issueNumber = issueNumber) + + if (content) { + article.content = content + article.renderedContent = marked(content) + } + + const data = await ArticleModel.findByIdAndUpdate(id, article, { + new: true + }).catch(err => { + ctx.log.error(err.message) + ctx.fail() + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } } -ctrl.delete = async (ctx, next) => {} +ctrl.delete = async (ctx, next) => { + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const data = await ArticleModel.remove({ _id: id }).catch(err => { + ctx.log.error(err.message) + ctx.fail() + }) -ctrl.like = async (ctx, next) => {} + if (data && data.result && data.result.ok) { + ctx.success() + } else { + ctx.fail() + } +} + +ctrl.like = async (ctx, next) => { + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + + const data = await ArticleModel.findByIdAndUpdate(id, { + $inc: { + 'meta.ups': 1 + } + }).catch(err => { + ctx.log.error(err.message) + ctx.fail() + }) + + if (data) { + ctx.success() + } else { + ctx.fail(-1, 'the article not found') + } +} /** * 获取相关文章 diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 14abf2b..4128613 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -18,8 +18,8 @@ function devAuth () { return async (ctx, next) => { if (!isProd && ctx.query._DEV_) { ctx._devauth_ = true - await next() } + await next() } } diff --git a/server/model/schema/article.js b/server/model/schema/article.js index f4dcc5b..d812405 100644 --- a/server/model/schema/article.js +++ b/server/model/schema/article.js @@ -23,7 +23,7 @@ const articleSchema = new mongoose.Schema({ // 标签 tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) - thumb: { uid: String, title: { type: String, default: '' }, url: { type: String, default: '' }, size: Number }, + thumb: { type: String, default: '' }, // 文章状态 ( 0 草稿 | 1 已发布 ) state: { type: Number, default: 0 }, // github issue diff --git a/server/validation.js b/server/validation.js index d766c44..338797c 100644 --- a/server/validation.js +++ b/server/validation.js @@ -11,7 +11,7 @@ const Validator = require('koa-bouncer').Validator const { isObjectId } = require('./util') Validator.addMethod('notEmpty', function (tip) { - this.isString('the "${this.key}" parameter should be String type') + this.isString(`the "${this.key}" parameter should be String type`) if (this.val().length === 0) { this.throwError(tip || `the "${this.key}" parameter should not be empty value`) } From 03b05ddb98a1821770859639104748222d943f4f Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 16:12:21 +0800 Subject: [PATCH 007/208] =?UTF-8?q?[add]=20add=20music=20api=20=20=20Flow?= =?UTF-8?q?=20style:=20!!map=20{=20Clark:=20Evans,=20Ingy:=20d=C3=B6t=20Ne?= =?UTF-8?q?t,=20Oren:=20Ben-Kiki=20}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 5 +++ package.json | 3 +- server/controller/article.js | 16 +++----- server/controller/index.js | 10 +++++ server/controller/music.js | 71 ++++++++++++++++++++++++++++++++++++ server/routes/backend.js | 14 +++---- server/routes/frontend.js | 16 ++++++-- 7 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 server/controller/index.js create mode 100644 server/controller/music.js diff --git a/package-lock.json b/package-lock.json index 2c3f744..d2624e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2914,6 +2914,11 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "simple-netease-cloud-music": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/simple-netease-cloud-music/-/simple-netease-cloud-music-0.1.8.tgz", + "integrity": "sha512-1vTRDzk0TYEUWrqdiUMl/cobJPj67YBmEaBfqqjnl+epC1ubhynqeqppy8bDIVX4giPItOWaI7ugz60rY1q+TA==" + }, "sliced": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", diff --git a/package.json b/package.json index 122d966..48c711a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "marked": "^0.3.6", "md5": "^2.2.1", "mongoose": "^4.11.12", - "mongoose-paginate": "^5.0.3" + "mongoose-paginate": "^5.0.3", + "simple-netease-cloud-music": "^0.1.8" }, "devDependencies": { "cross-env": "^5.0.5", diff --git a/server/controller/article.js b/server/controller/article.js index e4275d8..af7584a 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -9,10 +9,8 @@ const config = require('../config') const { ArticleModel, TagModel } = require('../model') const { marked, isObjectId } = require('../util') -const ctrl = { -} -ctrl.list = async (ctx, next) => { +exports.list = async (ctx, next) => { const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the "per_page" parameter should be greater than 0').val() const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'the "page" parameter should be greater than 0').val() const state = ctx.validateQuery('state').defaultTo(1).toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() @@ -92,7 +90,7 @@ ctrl.list = async (ctx, next) => { } } -ctrl.item = async (ctx, next) => { +exports.item = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() let data = null @@ -120,7 +118,7 @@ ctrl.item = async (ctx, next) => { } -ctrl.create = async (ctx, next) => { +exports.create = async (ctx, next) => { const title = ctx.validateBody('title') .required('the "title" parameter is required') .notEmpty() @@ -154,7 +152,7 @@ ctrl.create = async (ctx, next) => { } } -ctrl.update = async (ctx, next) => { +exports.update = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() const title = ctx.validateBody('title').optional().isString('the "title" parameter should be String type').val() const content = ctx.validateBody('content').optional().isString('the "content" parameter should be String type').val() @@ -193,7 +191,7 @@ ctrl.update = async (ctx, next) => { } } -ctrl.delete = async (ctx, next) => { +exports.delete = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() const data = await ArticleModel.remove({ _id: id }).catch(err => { ctx.log.error(err.message) @@ -207,7 +205,7 @@ ctrl.delete = async (ctx, next) => { } } -ctrl.like = async (ctx, next) => { +exports.like = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() const data = await ArticleModel.findByIdAndUpdate(id, { @@ -280,5 +278,3 @@ async function getSiblingArticles (ctx, data) { data.adjacent = { prev, next } } } - -module.exports = ctrl diff --git a/server/controller/index.js b/server/controller/index.js new file mode 100644 index 0000000..014399e --- /dev/null +++ b/server/controller/index.js @@ -0,0 +1,10 @@ +/** + * @desc Controllers entry + * @author Jooger + * @date 26 Sep 2017 + */ + +'use strict' + +exports.article = require('./article') +exports.music = require('./music') diff --git a/server/controller/music.js b/server/controller/music.js new file mode 100644 index 0000000..7c55731 --- /dev/null +++ b/server/controller/music.js @@ -0,0 +1,71 @@ +/** + * @desc Music controller + * @author Jooger + * @date 26 Sep 2017 + */ + +'use strict' + +const NeteseMusic = require('simple-netease-cloud-music') + +const neteaseMusic = new NeteseMusic() + +exports.list = async (ctx, next) => { + const playListId = ctx.validateQuery('play_list_id') + .required('the "play_list_id" parameter is required') + .notEmpty() + .isString('the "play_list_id" parameter should be String type') + .val() + + const { playlist } = await neteaseMusic.playlist(playListId) + + ctx.success(playlist) +} + +exports.item = async (ctx, next) => { + const songId = ctx.validateParam('song_id') + .required('the "song_id" parameter is required') + .notEmpty() + .isString('the "song_id" parameter should be String type') + .val() + + const { songs } = await neteaseMusic.song(songId) + + ctx.success(songs) +} + +exports.url = async (ctx, next) => { + const songId = ctx.validateParam('song_id') + .required('the "song_id" parameter is required') + .notEmpty() + .isString('the "song_id" parameter should be String type') + .val() + + const data = await neteaseMusic.url(songId) + + ctx.success(data) +} + +exports.lyric = async (ctx, next) => { + const coverId = ctx.validateParam('song_id') + .required('the "song_id" parameter is required') + .notEmpty() + .isString('the "song_id" parameter should be String type') + .val() + + const data = await neteaseMusic.lyric(songId) + + ctx.success(data) +} + +exports.cover = async (ctx, next) => { + const coverId = ctx.validateParam('cover_id') + .required('the "cover_id" parameter is required') + .notEmpty() + .isString('the "cover_id" parameter should be String type') + .val() + + const data = await neteaseMusic.picture(coverId) + + ctx.success(data) +} diff --git a/server/routes/backend.js b/server/routes/backend.js index fbf0117..cf37c0e 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -7,14 +7,14 @@ 'use strict' const router = require('koa-router')() -const articleCtrl = require('../controller/article') +const { article } = require('../controller') const { auth } = require('../middleware') -router.get('/articles', auth.isAuthenticated(), articleCtrl.list) -router.get('/articles/:id', auth.isAuthenticated(), articleCtrl.item) -router.post('/articles', auth.isAuthenticated(), articleCtrl.create) -router.patch('/articles/:id', auth.isAuthenticated(), articleCtrl.update) -router.delete('/articles/:id', auth.isAuthenticated(), articleCtrl.delete) -router.get('/articles/:id/like', auth.isAuthenticated(), articleCtrl.like) +router.get('/articles', auth.isAuthenticated(), article.list) +router.get('/articles/:id', auth.isAuthenticated(), article.item) +router.post('/articles', auth.isAuthenticated(), article.create) +router.patch('/articles/:id', auth.isAuthenticated(), article.update) +router.delete('/articles/:id', auth.isAuthenticated(), article.delete) +router.get('/articles/:id/like', auth.isAuthenticated(), article.like) module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 074d918..feee1e2 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -7,10 +7,18 @@ 'use strict' const router = require('koa-router')() -const articleCtrl = require('../controller/article') +const { article, music } = require('../controller') -router.get('/articles', articleCtrl.list) -router.get('/articles/:id', articleCtrl.item) -router.get('/articles/:id/like', articleCtrl.like) +// Article +router.get('/articles', article.list) +router.get('/articles/:id', article.item) +router.get('/articles/:id/like', article.like) + +// Music +router.get('/music/songs', music.list) +router.get('/music/songs/:song_id', music.item) +router.get('/music/songs/:song_id/url', music.url) +router.get('/music/songs/:song_id/lyric', music.lyric) +router.get('/music/songs/cover/:cover_id', music.cover) module.exports = router From 056bdc36a2d501b7054af145fe76017cf20e8388 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 17:00:03 +0800 Subject: [PATCH 008/208] [add] add tag api --- server/controller/article.js | 15 ++-- server/controller/index.js | 1 + server/controller/tag.js | 150 +++++++++++++++++++++++++++++++++++ server/model/schema/tag.js | 4 +- server/routes/backend.js | 10 ++- server/routes/frontend.js | 6 +- 6 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 server/controller/tag.js diff --git a/server/controller/article.js b/server/controller/article.js index af7584a..99bc8b8 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -72,7 +72,7 @@ exports.list = async (ctx, next) => { const articles = await ArticleModel.paginate(query, options).catch(err => { ctx.log.error(err.message) - ctx.fail() + return null }) if (articles) { @@ -104,7 +104,7 @@ exports.item = async (ctx, next) => { data = await queryPs.populate('tag').exec().catch(err => { ctx.log.error(err.message) - ctx.fail() + return null }) if (data) { @@ -142,7 +142,7 @@ exports.create = async (ctx, next) => { description }).save().catch(err => { ctx.log.error(err.message) - ctx.fail() + return null }) if (data) { @@ -181,7 +181,7 @@ exports.update = async (ctx, next) => { new: true }).catch(err => { ctx.log.error(err.message) - ctx.fail() + return null }) if (data) { @@ -195,7 +195,7 @@ exports.delete = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() const data = await ArticleModel.remove({ _id: id }).catch(err => { ctx.log.error(err.message) - ctx.fail() + return null }) if (data && data.result && data.result.ok) { @@ -214,7 +214,7 @@ exports.like = async (ctx, next) => { } }).catch(err => { ctx.log.error(err.message) - ctx.fail() + return null }) if (data) { @@ -237,6 +237,7 @@ async function getRelatedArticles (ctx, data) { .exec() .catch(err => { ctx.log.error('related articles access failed, err: ', err.message) + return null }) if (articles) { @@ -264,6 +265,7 @@ async function getSiblingArticles (ctx, data) { .exec() .catch(err => { ctx.log.error('adjacent articles access failed, err: ', err.message) + return null }) let next = await ArticleModel.findOne(query) .select('title createdAt thumb') @@ -272,6 +274,7 @@ async function getSiblingArticles (ctx, data) { .exec() .catch(err => { ctx.log.error('adjacent articles access failed, err: ', err.message) + return null }) prev = prev && prev.toObject() next = next && next.toObject() diff --git a/server/controller/index.js b/server/controller/index.js index 014399e..1df5a3d 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -7,4 +7,5 @@ 'use strict' exports.article = require('./article') +exports.tag = require('./tag') exports.music = require('./music') diff --git a/server/controller/tag.js b/server/controller/tag.js new file mode 100644 index 0000000..64f18f0 --- /dev/null +++ b/server/controller/tag.js @@ -0,0 +1,150 @@ +/** + * @desc Tag controller + * @author Jooger + * @date 26 Sep 2017 + */ + +'use strict' + +const { TagModel, ArticleModel } = require('../model') + +exports.list = async (ctx, next) => { + let data = await TagModel.find({}).sort('-createdAt').catch(err => { + ctx.log.error(err.message) + return [] + }) + + for (let i = 0; i < data.length; i++) { + if (data[i].toObject) { + data[i] = data[i].toObject() + } + const articles = await ArticleModel.find({ tag: data[i]._id }).exec().catch(err => { + ctx.log.error(err.message) + return [] + }) + data[i].count = articles.length + } + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.item = async (ctx, next) => { + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + + const data = await TagModel.findById(id).exec().catch(err => { + return null + }) + + if (data) { + data = data.toObject() + const articles = await ArticleModel.find({ tag: id }) + .select('-tag') + .exec() + .catch(err => { + ctx.log.error(err.message) + return [] + }) + data.articles = articles + data.articles_count = articles.length + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.create = async (ctx, next) => { + const name = ctx.validateBody('name') + .required('the "name" parameter is required') + .notEmpty() + .isString('the "name" parameter should be String type') + .val() + const description = ctx.validateBody('description') + .optional() + .isString('the "description" parameter should be String type') + .val() + const forbidden = ctx.validateBody('forbidden') + .defaultTo(0) + .toInt() + .isIn([0, 1], 'the "forbidden" parameter is not the expected value') + .val() + + const { length } = await TagModel.find({ name }).exec().catch(err => { + ctx.log.error(err.message) + return [] + }) + + if (!length) { + const data = await new TagModel({ + name, + description, + forbidden + }).save().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + return ctx.success(data) + } else { + ctx.fail() + } + } else { + ctx.fail(-1, `the tag(${name}) is already exist`) + } +} + +exports.update = async (ctx, next) => { + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const name = ctx.validateBody('name') + .optional() + .isString('the "name" parameter should be String type') + .val() + const description = ctx.validateBody('description') + .optional() + .isString('the "description" parameter should be String type') + .val() + const forbidden = ctx.validateBody('forbidden') + .optional() + .toInt() + .isIn([0, 1], 'the "forbidden" parameter is not the expected value') + .val() + + const tag = {} + console.log(forbidden) + name && (tag.name = name) + description && (tag.description = description) + if (forbidden !== undefined) { + tag.forbidden = forbidden + } + + const data = await TagModel.findByIdAndUpdate(id, tag, { + new: true + }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.delete = async (ctx, next) => { + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const data = await TagModel.remove({ _id: id }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data && data.result && data.result.ok) { + ctx.success() + } else { + ctx.fail() + } +} diff --git a/server/model/schema/tag.js b/server/model/schema/tag.js index 6c58032..e13a75d 100644 --- a/server/model/schema/tag.js +++ b/server/model/schema/tag.js @@ -10,10 +10,10 @@ const mongoose = require('mongoose') const tagSchema = new mongoose.Schema({ name: { type: String, required: true }, - description: String, + description: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, - forbidden: { type: Boolean, default: false } + forbidden: { type: Number, default: 0 } }) module.exports = tagSchema diff --git a/server/routes/backend.js b/server/routes/backend.js index cf37c0e..bdb046a 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -7,9 +7,10 @@ 'use strict' const router = require('koa-router')() -const { article } = require('../controller') +const { article, tag } = require('../controller') const { auth } = require('../middleware') +// Article router.get('/articles', auth.isAuthenticated(), article.list) router.get('/articles/:id', auth.isAuthenticated(), article.item) router.post('/articles', auth.isAuthenticated(), article.create) @@ -17,4 +18,11 @@ router.patch('/articles/:id', auth.isAuthenticated(), article.update) router.delete('/articles/:id', auth.isAuthenticated(), article.delete) router.get('/articles/:id/like', auth.isAuthenticated(), article.like) +// Tag +router.get('/tags', auth.isAuthenticated(), tag.list) +router.get('/tags/:id', auth.isAuthenticated(), tag.item) +router.post('/tags', auth.isAuthenticated(), tag.create) +router.patch('/tags/:id', auth.isAuthenticated(), tag.update) +router.delete('/tags/:id', auth.isAuthenticated(), tag.delete) + module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js index feee1e2..c93aba9 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -7,13 +7,17 @@ 'use strict' const router = require('koa-router')() -const { article, music } = require('../controller') +const { article, tag, music } = require('../controller') // Article router.get('/articles', article.list) router.get('/articles/:id', article.item) router.get('/articles/:id/like', article.like) +// Tag +router.get('/tags', tag.list) +router.get('/tags/:id', tag.item) + // Music router.get('/music/songs', music.list) router.get('/music/songs/:song_id', music.item) From cdb9ece36518de36f4a223c62d2b01100e6e4370 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 17:27:14 +0800 Subject: [PATCH 009/208] [add] add option schema --- server/model/schema/option.js | 39 ++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/server/model/schema/option.js b/server/model/schema/option.js index 1cbb602..2fd8af2 100644 --- a/server/model/schema/option.js +++ b/server/model/schema/option.js @@ -1,8 +1,45 @@ /** * @desc Option schema * @author Jooger - * @date 25 Sep 2017 + * @date 26 Sep 2017 */ 'use strict' +const mongoose = require('mongoose') + +const optionSchema = new mongoose.Schema({ + url: { type: String, default: '' }, + title: { type: String, default: '' }, + subtitle: { type: String, default: '' }, + welcome: { type: String, default: '' }, + description: [{ type: String, default: '' }], + banners: [{ type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }], + errorBanner: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, + hobby: [{ + name: { type: String, required: true }, + icon: { type: String, required: true } + }], + experience: [{ + time: { type: String, required: true }, + title: { type: String, required: true }, + subtitle: { type: String, default: '' } + }], + skill: [{ + title: { type: String, required: true }, + level: { type: String, required: true }, + icon: { type: String, required: true } + }], + contact: [{ + title: { type: String, required: true }, + url: { type: String, required: true }, + icon: { type: String, required: true } + }], + links: [{ + name: { type: String, required: true }, + github: { type: String, default: '' }, + site: { type: String, required: true } + }] +}) + +module.exports = optionSchema From f27988ac00ecd62974d0f2e312b612eadd8db4e9 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 17:59:28 +0800 Subject: [PATCH 010/208] [add] add option api --- api.md | 16 +++++++------- server/controller/index.js | 1 + server/controller/option.js | 39 +++++++++++++++++++++++++++++++++++ server/model/schema/index.js | 2 +- server/model/schema/option.js | 3 ++- server/mongo.js | 29 ++++++++++++++++++++++++++ server/routes/backend.js | 6 +++++- server/routes/frontend.js | 5 ++++- server/util/index.js | 5 +++++ 9 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 server/controller/option.js diff --git a/api.md b/api.md index 0d6e94e..dfde1d3 100644 --- a/api.md +++ b/api.md @@ -4,9 +4,9 @@ prefix: /api ### 文章 -> GET /frontend/articles 前台-文章列表 +> GET /articles 前台-文章列表 -> GET /frontend/articles/:id 前台-文章详情 +> GET /articles/:id 前台-文章详情 > GET /backend/articles 后台-文章列表 @@ -21,9 +21,9 @@ prefix: /api ### 标签 -> GET /frontend/tags 前台-标签列表 +> GET /tags 前台-标签列表 -> GET /frontend/tags/:id 前台-标签详情 +> GET /tags/:id 前台-标签详情 > GET /backend/tags 后台-标签列表 @@ -37,11 +37,11 @@ prefix: /api ### 评论 -> GET /frontend/comments 前台-评论列表 +> GET /comments 前台-评论列表 -> GET /frontend/comments/:id 前台-评论详情 +> GET /comments/:id 前台-评论详情 -> POST /frontend/comments 前台-发布评论 +> POST /comments 前台-发布评论 > GET /backend/comments 后台-评论列表 @@ -55,7 +55,7 @@ prefix: /api ### 全站配置 -> GET /frontend/option 前台-全站配置 +> GET /option 前台-全站配置 > GET /backend/option 后台-全站配置 diff --git a/server/controller/index.js b/server/controller/index.js index 1df5a3d..c49549a 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -9,3 +9,4 @@ exports.article = require('./article') exports.tag = require('./tag') exports.music = require('./music') +exports.option = require('./option') diff --git a/server/controller/option.js b/server/controller/option.js new file mode 100644 index 0000000..cca7ed5 --- /dev/null +++ b/server/controller/option.js @@ -0,0 +1,39 @@ +/** + * @desc Option controller + * @author Jooger + * @date 26 Sep 2017 + */ + +'use strict' + +const { OptionModel } = require('../model') + +exports.data = async (ctx, next) => { + const data = await OptionModel.findOne().exec().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.update = async (ctx, next) => { + const option = ctx.request.body + + const data = await OptionModel.findOneAndUpdate({}, option, { new: true }).exec().catch(err => { + ctx.log.error(err.message) + return null + }) + + console.log(data) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} diff --git a/server/model/schema/index.js b/server/model/schema/index.js index 916bf80..8544484 100644 --- a/server/model/schema/index.js +++ b/server/model/schema/index.js @@ -11,4 +11,4 @@ exports.comment = require('./comment') exports.tag = require('./tag') exports.admin = require('./admin') // exports.log = require('./log') -// exports.option = require('./option') +exports.option = require('./option') diff --git a/server/model/schema/option.js b/server/model/schema/option.js index 2fd8af2..351df16 100644 --- a/server/model/schema/option.js +++ b/server/model/schema/option.js @@ -39,7 +39,8 @@ const optionSchema = new mongoose.Schema({ name: { type: String, required: true }, github: { type: String, default: '' }, site: { type: String, required: true } - }] + }], + musicId: { type: String, default: '' } }) module.exports = optionSchema diff --git a/server/mongo.js b/server/mongo.js index 7ee39f8..f88027e 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -8,6 +8,8 @@ const mongoose = require('mongoose') const config = require('./config') +const { AdminModel, OptionModel } = require('./model') +const { debug } = require('./util') module.exports = function () { mongoose.Promise = global.Promise @@ -17,4 +19,31 @@ module.exports = function () { process.exit(0) } }) + + seedOption() + seedAdmin() +} + +function seedOption () { + OptionModel.findOne().exec().then(data => { + if (!data) { + createOption() + } + }) + + function createOption () { + new OptionModel().save().catch(err => debug(err.message)) + } +} + +function seedAdmin () { + AdminModel.findOne().exec().then(data => { + if (!data) { + createAdmin() + } + }) + + function createAdmin () { + new AdminModel().save().catch(err => debug(err.message)) + } } diff --git a/server/routes/backend.js b/server/routes/backend.js index bdb046a..164eb4b 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -7,7 +7,7 @@ 'use strict' const router = require('koa-router')() -const { article, tag } = require('../controller') +const { article, tag, option } = require('../controller') const { auth } = require('../middleware') // Article @@ -25,4 +25,8 @@ router.post('/tags', auth.isAuthenticated(), tag.create) router.patch('/tags/:id', auth.isAuthenticated(), tag.update) router.delete('/tags/:id', auth.isAuthenticated(), tag.delete) +// Option +router.get('/options', auth.isAuthenticated(), option.data) +router.patch('/options', auth.isAuthenticated(), option.update) + module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js index c93aba9..a90d2fc 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -7,7 +7,7 @@ 'use strict' const router = require('koa-router')() -const { article, tag, music } = require('../controller') +const { article, tag, music, option } = require('../controller') // Article router.get('/articles', article.list) @@ -25,4 +25,7 @@ router.get('/music/songs/:song_id/url', music.url) router.get('/music/songs/:song_id/lyric', music.lyric) router.get('/music/songs/cover/:cover_id', music.cover) +// Option +router.get('/options', option.data) + module.exports = router diff --git a/server/util/index.js b/server/util/index.js index 5866e64..a23db00 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -7,6 +7,11 @@ 'use strict' const mongoose = require('mongoose') +const debug = require('debug')(require('../../package.json').name) + +debug.enabled = true + +exports.debug = debug exports.marked = require('./marked') From a3b80d5bbcaf1714e1da737821a5cf2b9d3a6e3d Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 18:56:02 +0800 Subject: [PATCH 011/208] [update] update user api --- package-lock.json | 28 ++----- package.json | 2 +- server/config/development.js | 5 -- server/config/index.js | 2 +- server/config/production.js | 3 +- server/controller/index.js | 1 + server/controller/user.js | 91 +++++++++++++++++++++++ server/model/schema/index.js | 2 +- server/model/schema/{admin.js => user.js} | 13 ++-- server/mongo.js | 15 ++-- server/routes/backend.js | 6 +- server/routes/frontend.js | 5 +- server/util/index.js | 5 ++ 13 files changed, 134 insertions(+), 44 deletions(-) create mode 100644 server/controller/user.js rename server/model/schema/{admin.js => user.js} (54%) diff --git a/package-lock.json b/package-lock.json index d2624e5..56a311e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,6 +100,11 @@ "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, "better-assert": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", @@ -249,11 +254,6 @@ "supports-color": "2.0.0" } }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" - }, "chokidar": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", @@ -400,11 +400,6 @@ "which": "1.3.0" } }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" - }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -1694,7 +1689,8 @@ "is-buffer": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "dev": true }, "is-dotfile": { "version": "1.0.3", @@ -2230,16 +2226,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=" }, - "md5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", - "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", - "requires": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "1.1.5" - } - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", diff --git a/package.json b/package.json index 48c711a..151a379 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { + "bcryptjs": "^2.4.3", "debug": "^2.6.9", "highlight.js": "^9.12.0", "koa": "^2.2.0", @@ -24,7 +25,6 @@ "koa-router": "^7.1.1", "lodash": "^4.17.4", "marked": "^0.3.6", - "md5": "^2.2.1", "mongoose": "^4.11.12", "mongoose-paginate": "^5.0.3", "simple-netease-cloud-music": "^0.1.8" diff --git a/server/config/development.js b/server/config/development.js index f58cf16..dfe968c 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -11,10 +11,5 @@ const packageInfo = require('../../package.json') module.exports = { mongo: { uri: 'mongodb://127.0.0.1/jooger-me-dev' - }, - auth: { - cookie: { - maxAge: 60000 * 60 * 24 * 365 - } } } diff --git a/server/config/index.js b/server/config/index.js index 76d0dc8..9530a47 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -33,7 +33,7 @@ const baseConfig = { redis: {}, auth: { cookie: { - name: 'jooger.me' + name: 'jooger-me' }, secretKey: `${packageInfo.name} ${packageInfo.version}`, // token过期时间 diff --git a/server/config/production.js b/server/config/production.js index 485d047..b113f0a 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -14,8 +14,7 @@ module.exports = { }, auth: { cookie: { - domain: '.jooger.me', - maxAge: 60000 * 60 * 24 * 365 + domain: '.jooger.me' } } } diff --git a/server/controller/index.js b/server/controller/index.js index c49549a..90fca41 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -10,3 +10,4 @@ exports.article = require('./article') exports.tag = require('./tag') exports.music = require('./music') exports.option = require('./option') +exports.user = require('./user') diff --git a/server/controller/user.js b/server/controller/user.js new file mode 100644 index 0000000..a1700e9 --- /dev/null +++ b/server/controller/user.js @@ -0,0 +1,91 @@ +/** + * @desc User controlelr + * @author Jooger + * @date 26 Sep 2017 + */ + +'use strict' + +const jwt = require('jsonwebtoken') +const config = require('../config') +const { UserModel } = require('../model') +const { bhash, bcompare } = require('../util') + +exports.info = async (ctx, next) => { + const id = ctx.validateQuery('id').required('the "id" parameter is required').toString().isObjectId().val() + + let select = '-password' + + if (!ctx._isAuthenticated) { + select += ' -role' + } + + const data = await UserModel.findById(id) + .select(select) + .exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.login = async (ctx, next) => { + const name = ctx.validateBody('name') + .required('the "name" parameter is required') + .notEmpty() + .isString('the "name" parameter should be String type') + .val() + const password = ctx.validateBody('password') + .required('the "password" parameter is required') + .notEmpty() + .isString('the "password" parameter should be String type') + .val() + + const user = await UserModel.findOne({ name }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (user) { + const vertifyPassword = bcompare(password, user.password) + if (vertifyPassword) { + const { secretKey, expired, cookie } = config.auth + const token = signUserToken({ + id: user._id, + name: user.name + }) + ctx.cookies.set(cookie.name, token, { + signed: false, + domain: cookie.domain, + maxAge: expired + }) + ctx.success({ + id: user._id, + token + }, 'login success') + } else { + ctx.fail(-1, 'incorrect password') + } + } else { + ctx.fail(-1, 'user doesn\'t exist') + } +} + +exports.logout = async (ctx, next) => {} + +exports.update = async (ctx, next) => {} + +/** + * @desc jwt sign + * @param {Object} payload={} + * @param {Boolean} isLogin=false + */ +function signUserToken (payload = {}, isLogin = true) { + const { secretKey, expired } = config.auth + return jwt.sign(payload, secretKey, { expiresIn: isLogin ? expired : 0 }) +} diff --git a/server/model/schema/index.js b/server/model/schema/index.js index 8544484..0406842 100644 --- a/server/model/schema/index.js +++ b/server/model/schema/index.js @@ -9,6 +9,6 @@ exports.article = require('./article') exports.comment = require('./comment') exports.tag = require('./tag') -exports.admin = require('./admin') +exports.user = require('./user') // exports.log = require('./log') exports.option = require('./option') diff --git a/server/model/schema/admin.js b/server/model/schema/user.js similarity index 54% rename from server/model/schema/admin.js rename to server/model/schema/user.js index adf157e..1516b68 100644 --- a/server/model/schema/admin.js +++ b/server/model/schema/user.js @@ -7,18 +7,19 @@ 'use strict' const mongoose = require('mongoose') -const md5 = require('md5') const config = require('../../config') -const adminSchema = new mongoose.Schema({ +const userSchema = new mongoose.Schema({ name: { type: String, default: config.auth.defaultName, required: true }, password: { type: String, - default: md5(`${config.auth.secretKey}${config.auth.defaultPassword}`), - required: true + default: '' + // default: md5(`${config.auth.secretKey}${config.auth.defaultPassword}`) }, slogan: { type: String, default: '' }, - avatar: { type: String, default: '' } + avatar: { type: String, default: '' }, + // 角色 0 管理员 | 1 普通用户 + role: { type: Number, default: 1 } }) -module.exports = adminSchema +module.exports = userSchema diff --git a/server/mongo.js b/server/mongo.js index f88027e..8cc186d 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -8,8 +8,8 @@ const mongoose = require('mongoose') const config = require('./config') -const { AdminModel, OptionModel } = require('./model') -const { debug } = require('./util') +const { UserModel, OptionModel } = require('./model') +const { debug, bhash } = require('./util') module.exports = function () { mongoose.Promise = global.Promise @@ -37,13 +37,18 @@ function seedOption () { } function seedAdmin () { - AdminModel.findOne().exec().then(data => { + UserModel.findOne({ role: 0 }).exec().then(data => { if (!data) { createAdmin() } }) - + function createAdmin () { - new AdminModel().save().catch(err => debug(err.message)) + new UserModel({ + role: 0, + password: bhash(config.auth.defaultPassword) + }) + .save() + .catch(err => debug(err.message)) } } diff --git a/server/routes/backend.js b/server/routes/backend.js index 164eb4b..b8e1fa7 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -7,7 +7,7 @@ 'use strict' const router = require('koa-router')() -const { article, tag, option } = require('../controller') +const { article, tag, option, user } = require('../controller') const { auth } = require('../middleware') // Article @@ -29,4 +29,8 @@ router.delete('/tags/:id', auth.isAuthenticated(), tag.delete) router.get('/options', auth.isAuthenticated(), option.data) router.patch('/options', auth.isAuthenticated(), option.update) +// User +router.get('/user/info', auth.isAuthenticated(), user.info) +router.post('/user/login', user.login) + module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js index a90d2fc..b4d35aa 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -7,7 +7,7 @@ 'use strict' const router = require('koa-router')() -const { article, tag, music, option } = require('../controller') +const { article, tag, music, option, user } = require('../controller') // Article router.get('/articles', article.list) @@ -28,4 +28,7 @@ router.get('/music/songs/cover/:cover_id', music.cover) // Option router.get('/options', option.data) +// User +router.get('/user/info', user.info) + module.exports = router diff --git a/server/util/index.js b/server/util/index.js index a23db00..6fbb98a 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -6,6 +6,7 @@ 'use strict' +const bcrypt = require('bcryptjs') const mongoose = require('mongoose') const debug = require('debug')(require('../../package.json').name) @@ -19,3 +20,7 @@ exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) // 首字母大写 exports.firstUpperCase = (str = '') => str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) + +exports.bhash = (str = '') => bcrypt.hashSync(str, 8) + +exports.bcompare = (str, hash) => bcrypt.compareSync(str, hash) From dbab7383a84e39fdc36b8c50b9baaedff4a3760b Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 23:24:37 +0800 Subject: [PATCH 012/208] [update] update user api, add pm2 start script and process file --- .gitignore | 1 + bin/www | 4 +- ecosystem.config.js | 39 +++++++++++ package.json | 26 ++++++-- server/config/index.js | 4 +- server/controller/article.js | 9 ++- server/controller/option.js | 2 - server/controller/tag.js | 27 ++++---- server/controller/user.js | 117 ++++++++++++++++++++++++++++----- server/middleware/auth.js | 52 +++++++-------- server/middleware/header.js | 29 ++++++++ server/middleware/index.js | 1 + server/middleware/response.js | 2 +- server/model/schema/article.js | 2 +- server/model/schema/user.js | 6 +- server/routes/backend.js | 35 +++++----- server/routes/frontend.js | 2 +- server/routes/index.js | 5 +- server/util/index.js | 2 + 19 files changed, 275 insertions(+), 90 deletions(-) create mode 100644 ecosystem.config.js create mode 100644 server/middleware/header.js diff --git a/.gitignore b/.gitignore index 7813b29..44b908e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Logs +logs *.log # Runtime data diff --git a/bin/www b/bin/www index 609910e..e9f7e97 100755 --- a/bin/www +++ b/bin/www @@ -67,11 +67,11 @@ function onError(error) { // handle specific listen errors with friendly messages switch (error.code) { case 'EACCES': - console.error(bind + ' requires elevated privileges') + debug(bind + ' requires elevated privileges') process.exit(1) break case 'EADDRINUSE': - console.error(bind + ' is already in use') + debug(bind + ' is already in use') process.exit(1) break default: diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..e470351 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,39 @@ +/** + * @desc PM2 + * @author Jooger + * @date 26 Sep 2017 + */ + +'use strict' + +const packageInfo = require('./package.json') + +module.exports = { + apps: { + name: packageInfo.name, + script: './bin/www', + cwd: __dirname, + watch: true, + ignore_watch: ["[\/\\]\./", "node_modules"], + env: { + NODE_ENV: 'production' + }, + env_production: { + NODE_ENV: "production" + }, + log_date_format: "YYYY-MM-DD HH:mm Z", + out_file: './logs/pm2-out.log', + error_file: './logs/pm2-error.log', + pid_file: './logs/jooger.me-server.pid' + }, + deploy: { + production: { + user : 'root', + host : 'jooger.me', + ref : 'origin/master', + repo : packageInfo.repository.url, + path : '/var/www/' + packageInfo.name, + 'post-deploy' : 'cnpm install && pm2 stop all && pm2 reload ecosystem.config.js --env production && pm2 start all' + } + } +} diff --git a/package.json b/package.json index 151a379..0340a63 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,29 @@ { "name": "jooger.me-server", - "version": "0.1.0", + "version": "1.0.0", "private": true, "scripts": { - "dev": "cross-env NODE_ENV=development ./node_modules/.bin/nodemon bin/www", - "debug": "cross-env NODE_ENV=development ./node_modules/.bin/nodemon --inspect bin/www", - "pm2": "pm2 start bin/www --name='jooger.me-server'", - "test": "echo \"Error: no test specified\" && exit 1" + "dev": "cross-env NODE_ENV=development nodemon bin/www", + "debug": "cross-env NODE_ENV=development nodemon --inspect bin/www", + "pm2": "pm2 startOrReload ecosystem.config.js", + "test": "echo \"Error: no test specified\" && exit 1", + "deploy": "pm2 deploy ecosystem.config.js production" }, + "repository": { + "type": "https", + "url": "https://github.com/jo0ger/jooger.me-server.git" + }, + "keywords": [ + "jooger.me", + "server", + "api" + ], + "author": "Jooger", + "license": "MIT", + "bugs": { + "url": "https://github.com/jo0ger/jooger.me-server/issues" + }, + "bin": "./node_modules/.bin/", "dependencies": { "bcryptjs": "^2.4.3", "debug": "^2.6.9", diff --git a/server/config/index.js b/server/config/index.js index 9530a47..de23b07 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -11,6 +11,8 @@ const _ = require('lodash') const packageInfo = require('../../package.json') const baseConfig = { + name: packageInfo.name, + version: packageInfo.version, env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3000, @@ -37,7 +39,7 @@ const baseConfig = { }, secretKey: `${packageInfo.name} ${packageInfo.version}`, // token过期时间 - expired: 60 * 60 * 24 * 365, + expired: 60000 * 60 * 24 * 365, defaultName: 'admin', defaultPassword: 'admin', // 允许请求的域名 diff --git a/server/controller/article.js b/server/controller/article.js index 99bc8b8..24e308f 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -8,7 +8,7 @@ const config = require('../config') const { ArticleModel, TagModel } = require('../model') -const { marked, isObjectId } = require('../util') +const { marked, isObjectId, createObjectId } = require('../util') exports.list = async (ctx, next) => { const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the "per_page" parameter should be greater than 0').val() @@ -69,7 +69,7 @@ exports.list = async (ctx, next) => { // 文章列表不需要content和state options.select = '-content -renderedContent -state' } - + const articles = await ArticleModel.paginate(query, options).catch(err => { ctx.log.error(err.message) return null @@ -168,10 +168,13 @@ exports.update = async (ctx, next) => { keywords && (article.keywords = keywords) description && (article.description = description) tag && (article.tag = tag) - state && (article.state = state) thumb && (article.thumb = thumb) issueNumber && (article.issueNumber = issueNumber) + if (state !== undefined) { + article.state = state + } + if (content) { article.content = content article.renderedContent = marked(content) diff --git a/server/controller/option.js b/server/controller/option.js index cca7ed5..fd57bc8 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -29,8 +29,6 @@ exports.update = async (ctx, next) => { return null }) - console.log(data) - if (data) { ctx.success(data) } else { diff --git a/server/controller/tag.js b/server/controller/tag.js index 64f18f0..403d7a0 100644 --- a/server/controller/tag.js +++ b/server/controller/tag.js @@ -11,21 +11,20 @@ const { TagModel, ArticleModel } = require('../model') exports.list = async (ctx, next) => { let data = await TagModel.find({}).sort('-createdAt').catch(err => { ctx.log.error(err.message) - return [] + return null }) - - for (let i = 0; i < data.length; i++) { - if (data[i].toObject) { - data[i] = data[i].toObject() - } - const articles = await ArticleModel.find({ tag: data[i]._id }).exec().catch(err => { - ctx.log.error(err.message) - return [] - }) - data[i].count = articles.length - } - + if (data) { + for (let i = 0; i < data.length; i++) { + if (data[i].toObject) { + data[i] = data[i].toObject() + } + const articles = await ArticleModel.find({ tag: data[i]._id }).exec().catch(err => { + ctx.log.error(err.message) + return [] + }) + data[i].count = articles.length + } ctx.success(data) } else { ctx.fail() @@ -114,7 +113,7 @@ exports.update = async (ctx, next) => { .val() const tag = {} - console.log(forbidden) + name && (tag.name = name) description && (tag.description = description) if (forbidden !== undefined) { diff --git a/server/controller/user.js b/server/controller/user.js index a1700e9..ace204c 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -11,8 +11,29 @@ const config = require('../config') const { UserModel } = require('../model') const { bhash, bcompare } = require('../util') -exports.info = async (ctx, next) => { - const id = ctx.validateQuery('id').required('the "id" parameter is required').toString().isObjectId().val() +exports.list = async (ctx, next) => { + let select = '-password' + if (!ctx._isAuthenticated) { + select += ' -createdAt -updatedAt' + } + + const data = await UserModel.find({}) + .sort('-createdAt') + .select(select) + .catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.item = async (ctx, next) => { + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() let select = '-password' @@ -40,11 +61,11 @@ exports.login = async (ctx, next) => { .notEmpty() .isString('the "name" parameter should be String type') .val() - const password = ctx.validateBody('password') - .required('the "password" parameter is required') - .notEmpty() - .isString('the "password" parameter should be String type') - .val() + const password = ctx.validateBody('password') + .required('the "password" parameter is required') + .notEmpty() + .isString('the "password" parameter should be String type') + .val() const user = await UserModel.findOne({ name }).catch(err => { ctx.log.error(err.message) @@ -54,16 +75,13 @@ exports.login = async (ctx, next) => { if (user) { const vertifyPassword = bcompare(password, user.password) if (vertifyPassword) { - const { secretKey, expired, cookie } = config.auth + const { expired, cookie } = config.auth const token = signUserToken({ id: user._id, name: user.name }) - ctx.cookies.set(cookie.name, token, { - signed: false, - domain: cookie.domain, - maxAge: expired - }) + ctx.cookies.set(cookie.name, token, { domain: cookie.domain, maxAge: expired, httpOnly: true }) + ctx.cookies.set('user_id', user._id, { domain: cookie.domain, maxAge: expired }) ctx.success({ id: user._id, token @@ -76,9 +94,78 @@ exports.login = async (ctx, next) => { } } -exports.logout = async (ctx, next) => {} +exports.logout = async (ctx, next) => { + const { expired, cookie } = config.auth + const token = signUserToken({ + id: ctx._user._id, + name: ctx._user.name + }, false) + ctx.cookies.set(cookie.name, token, { + maxAge: 0 + }) + ctx.success() +} + +exports.update = async (ctx, next) => { + const name = ctx.validateBody('name').optional().isString('the "name" parameter should be String type').val() + const password = ctx.validateBody('password').optional().isString('the "password" parameter should be String type').val() + const slogan = ctx.validateBody('slogan').optional().isString('the "slogan" parameter should be String type').val() + const description = ctx.validateBody('description').optional().isString('the "description" parameter should be String type').val() + const avatar = ctx.validateBody('avatar').optional().isString('the "avatar" parameter should be String type').val() + const role = ctx.validateBody('role').optional().toInt().isIn([0, 1], 'the "role" parameter is not the expected value').val() + + const user = {} + + name && (user.name = name) + slogan && (user.slogan = slogan) + description && (user.description = description) + avatar && (user.avatar = avatar) + + if (role !== undefined) { + user.role = role + } + + if (password !== undefined) { + const oldPassword = ctx.validateBody('old_password') + .required('the "old_password" parameter is required') + .notEmpty() + .isString('the "old_password" parameter should be String type') + .val() + + const vertifyPassword = bcompare(oldPassword, ctx._user.password) + if (!vertifyPassword) { + return ctx.fail(-1, 'old password is not correct') + } + user.password = bhash(password) + } + + const data = await UserModel.findByIdAndUpdate(ctx._user._id, user, { + new: true + }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.delete = async (ctx, next) => { + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const data = await UserModel.remove({ _id: id }).catch(err => { + ctx.log.error(err.message) + return null + }) -exports.update = async (ctx, next) => {} + if (data && data.result && data.result.ok) { + ctx.success() + } else { + ctx.fail() + } +} /** * @desc jwt sign diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 4128613..e766c95 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc Auth middleware * @author Jooger * @date 25 Sep 2017 */ @@ -17,37 +17,31 @@ const isProd = process.env.NODE_ENV === 'production' function devAuth () { return async (ctx, next) => { if (!isProd && ctx.query._DEV_) { - ctx._devauth_ = true + ctx._devauth = true } await next() } } function verifyToken () { - return compose([ - async (ctx, next) => { - if (ctx._devauth_) { - return await next() - } - const token = ctx.cookies.get(config.auth.cookie.name, { signed: true }) - if (token) { - try { - const decodedToken = await jwt.verify(token, config.auth.secretKey) - if (decodedToken.exp > Math.floor(Date.now() / 1000)) { - // 已验证权限 - await next() - } - } catch (err) { - ctx.fail(401, err.message) + return async (ctx, next) => { + if (ctx._devauth) { + return await next() + } + const token = ctx.cookies.get(config.auth.cookie.name, { signed: true }) + if (token) { + try { + const decodedToken = await jwt.verify(token, config.auth.secretKey) + if (decodedToken.exp > Math.floor(Date.now() / 1000)) { + // 已验证权限 + await next() } + } catch (err) { + ctx.fail(401, err.message) } - ctx.fail(401) - }, - koajwt({ - secret: config.auth.secretKey, - passthrough: true - }) - ]) + } + ctx.fail(401) + } } exports.isAuthenticated = () => { @@ -55,7 +49,7 @@ exports.isAuthenticated = () => { devAuth(), verifyToken(), async (ctx, next) => { - if (ctx._devauth_) { + if (ctx._devauth) { return await next() } else if (!ctx.state.user) { ctx.fail(401) @@ -64,11 +58,15 @@ exports.isAuthenticated = () => { await next() }, async (ctx, next) => { - if (ctx._devauth_) { + if (ctx._devauth) { ctx._isAuthenticated = true return await next() } - const user = await UserModel.findById(ctx.state.user._id) + const userId = ctx.cookies.get('user_id', { domain: config.cookie.domain }) + const user = await UserModel.findById(userId).exec().catch(err => { + ctx.log.error(err.message) + return null + }) if (!user) { ctx.fail(401) return diff --git a/server/middleware/header.js b/server/middleware/header.js new file mode 100644 index 0000000..938c210 --- /dev/null +++ b/server/middleware/header.js @@ -0,0 +1,29 @@ +/** + * @desc 设置相应头 + * @author Jooger + * @date 26 Sep 2017 + */ + +'use strict' + +const config = require('../config') + +module.exports = async (ctx, next) => { + const { request, response } = ctx + const allowedOrigins = config.auth.allowedOrigins + const origin = request.get('origin') || '' + const allowed = origin.includes('localhost') || request.query._DEV_ || allowedOrigins.find(item => origin.includes(item)) + if (allowed) { + response.set('Access-Control-Allow-Origin', origin) + } + response.set("Access-Control-Allow-Headers", "Authorization, Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With") + response.set("Access-Control-Allow-Methods", "PUT,PATCH,POST,GET,DELETE,OPTIONS") + response.set("Access-Control-Allow-Credentials", true) + response.set("Content-Type", "application/json;charset=utf-8") + response.set("X-Powered-By", `${config.name}/${config.version}`) + + if (request.method === 'OPTIONS') { + return ctx.success('ok') + } + await next() +} diff --git a/server/middleware/index.js b/server/middleware/index.js index 9adf4ed..2a64296 100644 --- a/server/middleware/index.js +++ b/server/middleware/index.js @@ -9,3 +9,4 @@ exports.error = require('./error') exports.response = require('./response') exports.auth = require('./auth') +exports.header = require('./header') diff --git a/server/middleware/response.js b/server/middleware/response.js index 8f31bef..6e7f7a3 100644 --- a/server/middleware/response.js +++ b/server/middleware/response.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc Reponse middleware * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/model/schema/article.js b/server/model/schema/article.js index d812405..15535f0 100644 --- a/server/model/schema/article.js +++ b/server/model/schema/article.js @@ -23,7 +23,7 @@ const articleSchema = new mongoose.Schema({ // 标签 tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) - thumb: { type: String, default: '' }, + thumb: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, // 文章状态 ( 0 草稿 | 1 已发布 ) state: { type: Number, default: 0 }, // github issue diff --git a/server/model/schema/user.js b/server/model/schema/user.js index 1516b68..2a0031e 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -17,9 +17,11 @@ const userSchema = new mongoose.Schema({ // default: md5(`${config.auth.secretKey}${config.auth.defaultPassword}`) }, slogan: { type: String, default: '' }, - avatar: { type: String, default: '' }, + avatar: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, // 角色 0 管理员 | 1 普通用户 - role: { type: Number, default: 1 } + role: { type: Number, default: 1 }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now } }) module.exports = userSchema diff --git a/server/routes/backend.js b/server/routes/backend.js index b8e1fa7..19695c7 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -9,28 +9,33 @@ const router = require('koa-router')() const { article, tag, option, user } = require('../controller') const { auth } = require('../middleware') +const isAuthenticated = auth.isAuthenticated() // Article -router.get('/articles', auth.isAuthenticated(), article.list) -router.get('/articles/:id', auth.isAuthenticated(), article.item) -router.post('/articles', auth.isAuthenticated(), article.create) -router.patch('/articles/:id', auth.isAuthenticated(), article.update) -router.delete('/articles/:id', auth.isAuthenticated(), article.delete) -router.get('/articles/:id/like', auth.isAuthenticated(), article.like) +router.get('/articles', isAuthenticated, article.list) +router.get('/articles/:id', isAuthenticated, article.item) +router.post('/articles', isAuthenticated, article.create) +router.patch('/articles/:id', isAuthenticated, article.update) +router.delete('/articles/:id', isAuthenticated, article.delete) +router.get('/articles/:id/like', isAuthenticated, article.like) // Tag -router.get('/tags', auth.isAuthenticated(), tag.list) -router.get('/tags/:id', auth.isAuthenticated(), tag.item) -router.post('/tags', auth.isAuthenticated(), tag.create) -router.patch('/tags/:id', auth.isAuthenticated(), tag.update) -router.delete('/tags/:id', auth.isAuthenticated(), tag.delete) +router.get('/tags', isAuthenticated, tag.list) +router.get('/tags/:id', isAuthenticated, tag.item) +router.post('/tags', isAuthenticated, tag.create) +router.patch('/tags/:id', isAuthenticated, tag.update) +router.delete('/tags/:id', isAuthenticated, tag.delete) // Option -router.get('/options', auth.isAuthenticated(), option.data) -router.patch('/options', auth.isAuthenticated(), option.update) +router.get('/options', isAuthenticated, option.data) +router.patch('/options', isAuthenticated, option.update) // User -router.get('/user/info', auth.isAuthenticated(), user.info) -router.post('/user/login', user.login) +router.get('/users', isAuthenticated, user.list) +router.post('/users/login', user.login) +router.get('/users/logout', isAuthenticated, user.logout) +router.get('/users/:id', isAuthenticated, user.item) +router.patch('/users/:id', isAuthenticated, user.update) +router.delete('/users/:id', isAuthenticated, user.delete) module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js index b4d35aa..69efe39 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -29,6 +29,6 @@ router.get('/music/songs/cover/:cover_id', music.cover) router.get('/options', option.data) // User -router.get('/user/info', user.info) +router.get('/users/:id', user.item) module.exports = router diff --git a/server/routes/index.js b/server/routes/index.js index 749f63b..a082256 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -11,10 +11,13 @@ const router = require('koa-router')({ }) const frontend = require('./frontend') const backend = require('./backend') +const { header } = require('../middleware') module.exports = app => { - router.use(frontend.routes(), frontend.allowedMethods()) + router.use('*', header) + router.use('/backend', backend.routes(), backend.allowedMethods()) + router.use(frontend.routes(), frontend.allowedMethods()) router.all('*', (ctx,next)=> { ctx.fail(404, `${ctx.path} 不支持 ${ctx.method} 请求类型`) diff --git a/server/util/index.js b/server/util/index.js index 6fbb98a..e1c519c 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -16,6 +16,8 @@ exports.debug = debug exports.marked = require('./marked') +exports.createObjectId = () => mongoose.Types.ObjectId() + exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) // 首字母大写 From 2059dfae01463bbf0a9129b8a2482a242602226d Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 10:26:25 -0500 Subject: [PATCH 013/208] Initial commit --- LICENSE | 21 +++++++++++++++++++++ README.md | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..453fb94 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Jooger + +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/README.md b/README.md new file mode 100644 index 0000000..2b324d6 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# jooger.me-server +jooger.me's api server From 0b47ebecb9082c4debd75fbae18568f9f684657e Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 23:33:27 +0800 Subject: [PATCH 014/208] [update] update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b324d6..49268ff 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # jooger.me-server -jooger.me's api server + +My blog's api server build by koa2 and mongoose From 821bcfdef02aacbec16eac07bf8ce2dd48d22cdd Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 26 Sep 2017 23:46:37 +0800 Subject: [PATCH 015/208] [update] update pm2 process file --- ecosystem.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecosystem.config.js b/ecosystem.config.js index e470351..9a6b5bf 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -32,7 +32,7 @@ module.exports = { host : 'jooger.me', ref : 'origin/master', repo : packageInfo.repository.url, - path : '/var/www/' + packageInfo.name, + path : '/root/www/' + packageInfo.name, 'post-deploy' : 'cnpm install && pm2 stop all && pm2 reload ecosystem.config.js --env production && pm2 start all' } } From 6aa08f4efeddb40eb45ed3da7290662177686230 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 27 Sep 2017 14:56:56 +0800 Subject: [PATCH 016/208] [add] add github auth service --- bin/www | 2 +- package-lock.json | 112 +++++++++++++++++++++++++++++- package.json | 5 +- server/app.js | 11 ++- server/config/index.js | 19 +++-- server/config/production.js | 2 +- server/controller/auth.js | 93 +++++++++++++++++++++++++ server/controller/index.js | 1 + server/controller/user.js | 63 ----------------- server/middleware/auth.js | 79 --------------------- server/middleware/authenticate.js | 98 ++++++++++++++++++++++++++ server/middleware/index.js | 2 +- server/model/schema/user.js | 13 +++- server/mongo.js | 5 +- server/routes/backend.js | 12 ++-- server/routes/frontend.js | 11 ++- server/service/github-passport.js | 84 ++++++++++++++++++++++ server/service/index.js | 9 +++ server/util/index.js | 20 +++++- server/util/sign-token.js | 15 ++++ server/validation.js | 2 +- 21 files changed, 489 insertions(+), 169 deletions(-) create mode 100644 server/controller/auth.js delete mode 100644 server/middleware/auth.js create mode 100644 server/middleware/authenticate.js create mode 100644 server/service/github-passport.js create mode 100644 server/service/index.js create mode 100644 server/util/sign-token.js diff --git a/bin/www b/bin/www index e9f7e97..6e360c0 100755 --- a/bin/www +++ b/bin/www @@ -6,7 +6,7 @@ const http = require('http') const app = require('../server/app') -const debug = require('debug')(require('../package.json').name) +const debug = require('../server/util').setDebug() const config = require('../server/config') debug.enabled = true diff --git a/package-lock.json b/package-lock.json index 56a311e..2f2e25e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jooger.me-server", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -370,6 +370,11 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "crc": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz", + "integrity": "sha1-mLi6fUiWZbo5efWbITgTdBAaGWQ=" + }, "create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", @@ -1692,6 +1697,11 @@ "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", "dev": true }, + "is-class": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/is-class/-/is-class-0.0.4.tgz", + "integrity": "sha1-4FdFFwW7NOOePjNZjJOpg3KWtzY=" + }, "is-dotfile": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", @@ -1790,6 +1800,16 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-type-of": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-type-of/-/is-type-of-1.2.0.tgz", + "integrity": "sha512-10ezBXuEDp3Fp/jPCaVd4hSrAEj2lPyr1LT7+cWi9HCLd15wbh9X8dJfTDB+ZgkZSCGTG2TF6f61ugI5mSlhDA==", + "requires": { + "core-util-is": "1.0.2", + "is-class": "0.0.4", + "isstream": "0.1.2" + } + }, "is-windows": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.1.tgz", @@ -1821,6 +1841,11 @@ "isarray": "1.0.0" } }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, "joi": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", @@ -2049,6 +2074,14 @@ "swig": "1.4.2" } }, + "koa-passport": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-passport/-/koa-passport-4.0.0.tgz", + "integrity": "sha512-o7KHuioB7VPiAoEFiu1W3CRtiBTWilfBGbTQi4cb5DIbPwETG0kfC8q2b9PuPIhSt3BVN4gF3o6Qu3VsdiuwLg==", + "requires": { + "passport": "0.4.0" + } + }, "koa-router": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.2.1.tgz", @@ -2071,6 +2104,17 @@ } } }, + "koa-session": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/koa-session/-/koa-session-5.5.0.tgz", + "integrity": "sha512-+f5YY137wuu4RtSaalWJdUYd80S1v79uWcecIRGLKVtljTuv6fFzPlvTmWl1V0MaSin8rEXP9urgbIVQEN/YVA==", + "requires": { + "crc": "3.5.0", + "debug": "2.6.9", + "is-type-of": "1.2.0", + "uid-safe": "2.1.5" + } + }, "koa-unless": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/koa-unless/-/koa-unless-1.0.0.tgz", @@ -2514,6 +2558,11 @@ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + }, "object.omit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", @@ -2589,6 +2638,39 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" }, + "passport": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", + "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", + "requires": { + "passport-strategy": "1.0.0", + "pause": "0.0.1" + } + }, + "passport-github": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz", + "integrity": "sha1-jOHj/NYa11eOsd9ZWDnkrqEjVdQ=", + "requires": { + "passport-oauth2": "1.4.0" + } + }, + "passport-oauth2": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", + "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", + "requires": { + "oauth": "0.9.15", + "passport-strategy": "1.0.0", + "uid2": "0.0.3", + "utils-merge": "1.0.1" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "passthrough-counter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passthrough-counter/-/passthrough-counter-1.0.0.tgz", @@ -2620,6 +2702,11 @@ } } }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, "pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -2672,6 +2759,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz", "integrity": "sha1-wx2bdOwn33XlQ6hseHKO2NRiNgc=" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", @@ -3082,6 +3174,19 @@ "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=" }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "1.0.0" + } + }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + }, "undefsafe": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-0.0.3.tgz", @@ -3138,6 +3243,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, "uuid": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", diff --git a/package.json b/package.json index 0340a63..7131c52 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "keywords": [ "jooger.me", - "server", + "server", "api" ], "author": "Jooger", @@ -38,11 +38,14 @@ "koa-jwt": "^3.2.2", "koa-logger": "^2.0.1", "koa-onerror": "^1.2.1", + "koa-passport": "^4.0.0", "koa-router": "^7.1.1", + "koa-session": "^5.5.0", "lodash": "^4.17.4", "marked": "^0.3.6", "mongoose": "^4.11.12", "mongoose-paginate": "^5.0.3", + "passport-github": "^1.1.0", "simple-netease-cloud-music": "^0.1.8" }, "devDependencies": { diff --git a/server/app.js b/server/app.js index a721394..6540dee 100644 --- a/server/app.js +++ b/server/app.js @@ -11,20 +11,27 @@ const json = require('koa-json') const logger = require('koa-logger') const onerror = require('koa-onerror') const bouncer = require('koa-bouncer') +const session = require('koa-session') +const passport = require('koa-passport') const compress = require('koa-compress') const bodyparser = require('koa-bodyparser') const koaBunyanLogger = require('koa-bunyan-logger') const middlewares = require('./middleware') +const config = require('./config') const app = new Koa() +// connect mongodb require('./mongo')() +// load custom validations bouncer.Validator = require('./validation') // error handler onerror(app) +app.keys = config.auth.secrets + // middlewares app.use(bodyparser({ enableTypes:['json', 'form', 'text'] @@ -32,10 +39,12 @@ app.use(bodyparser({ app.use(json()) app.use(logger()) app.use(koaBunyanLogger()) -app.use(compress()) app.use(bouncer.middleware()) app.use(middlewares.response) app.use(middlewares.error) +app.use(session(config.auth.session, app)) +app.use(passport.initialize()) +app.use(compress()) // routes require('./routes')(app) diff --git a/server/config/index.js b/server/config/index.js index de23b07..1697f13 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -15,7 +15,7 @@ const baseConfig = { version: packageInfo.version, env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), - port: process.env.PORT || 3000, + port: process.env.PORT || 3001, pageSize: 12, codeMap: { '-1': 'fail', @@ -34,12 +34,12 @@ const baseConfig = { // TODO: Redis redis: {}, auth: { - cookie: { - name: 'jooger-me' + session: { + key: 'jooger.me.sid', + maxAge: 60000 * 60 * 24 * 7, + signed: false }, - secretKey: `${packageInfo.name} ${packageInfo.version}`, - // token过期时间 - expired: 60000 * 60 * 24 * 365, + secrets: `${packageInfo.name} ${packageInfo.version}`, defaultName: 'admin', defaultPassword: 'admin', // 允许请求的域名 @@ -49,6 +49,13 @@ const baseConfig = { 'blog.jooger.me', 'admin.jooger.me' ] + }, + sns: { + github: { + clientID: process.env.GITHUB_CLIENT_ID || '5b4d4a7945347d0fd2e2', + clientSecret: process.env.GITHUB_CLIENT_SECRET || '8771bd9ae52749cc15b0c9e2c6cb4ecd7f39d9da', + callbackURL: process.env.GITHUB_CALLBACK_URL || 'http://127.0.0.1:3001/api/auth/github/login/callback' + } } } diff --git a/server/config/production.js b/server/config/production.js index b113f0a..89763ab 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -13,7 +13,7 @@ module.exports = { uri: 'mongodb://127.0.0.1/jooger-me' }, auth: { - cookie: { + session: { domain: '.jooger.me' } } diff --git a/server/controller/auth.js b/server/controller/auth.js new file mode 100644 index 0000000..3583e3e --- /dev/null +++ b/server/controller/auth.js @@ -0,0 +1,93 @@ +/** + * @desc Auth controller + * @author Jooger + * @date 27 Sep 2017 + */ + +'use strict' + +const jwt = require('jsonwebtoken') +const passport = require('koa-passport') +const config = require('../config') +const { UserModel } = require('../model') +const { bhash, bcompare, setDebug, signToken } = require('../util') +const debug = setDebug('auth:github') + +const { githubPassport } = require('../service') + +githubPassport.init(UserModel, config) + +exports.localLogin = async (ctx, next) => { + const name = ctx.validateBody('name') + .required('the "name" parameter is required') + .notEmpty() + .isString('the "name" parameter should be String type') + .val() + const password = ctx.validateBody('password') + .required('the "password" parameter is required') + .notEmpty() + .isString('the "password" parameter should be String type') + .val() + + const user = await UserModel.findOne({ name }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (user) { + const vertifyPassword = bcompare(password, user.password) + if (vertifyPassword) { + const { session } = config.auth + const token = signToken({ + id: user._id, + name: user.name + }) + ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: true }) + ctx.cookies.set('user_id', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) + debug('login success, user: ', user._id) + ctx.success({ + id: user._id, + token + }, 'login success') + } else { + ctx.fail(-1, 'incorrect password') + } + } else { + ctx.fail(-1, 'user doesn\'t exist') + } +} + +exports.logout = async (ctx, next) => { + const { session } = config.auth + const token = signToken({ + id: ctx._user._id, + name: ctx._user.name + }, false) + ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: 0, httpOnly: true }) + ctx.cookies.set('user_id', ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) + debug('logout success, user: ', ctx._user._id) + ctx.success(null, 'logout success') +} + +// github login +exports.githubLogin = async (ctx, next) => { + await passport.authenticate('github', { + session: false + }, (err, user) => { + debug('github auth callback start') + const redirectUrl = ctx.session.passport.redirectUrl || '/' + const cookieDomain = config.auth.session.domain || null + + const { session } = config.auth + const token = signToken({ + id: user._id, + name: user.name + }) + ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: true }) + ctx.cookies.set('user_id', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) + + debug('github auth callback finish') + debug('github login success, user: ', user._id) + return ctx.redirect(redirectUrl) + })(ctx) +} diff --git a/server/controller/index.js b/server/controller/index.js index 90fca41..0414f1d 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -11,3 +11,4 @@ exports.tag = require('./tag') exports.music = require('./music') exports.option = require('./option') exports.user = require('./user') +exports.auth = require('./auth') diff --git a/server/controller/user.js b/server/controller/user.js index ace204c..da0df67 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -6,8 +6,6 @@ 'use strict' -const jwt = require('jsonwebtoken') -const config = require('../config') const { UserModel } = require('../model') const { bhash, bcompare } = require('../util') @@ -55,57 +53,6 @@ exports.item = async (ctx, next) => { } } -exports.login = async (ctx, next) => { - const name = ctx.validateBody('name') - .required('the "name" parameter is required') - .notEmpty() - .isString('the "name" parameter should be String type') - .val() - const password = ctx.validateBody('password') - .required('the "password" parameter is required') - .notEmpty() - .isString('the "password" parameter should be String type') - .val() - - const user = await UserModel.findOne({ name }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (user) { - const vertifyPassword = bcompare(password, user.password) - if (vertifyPassword) { - const { expired, cookie } = config.auth - const token = signUserToken({ - id: user._id, - name: user.name - }) - ctx.cookies.set(cookie.name, token, { domain: cookie.domain, maxAge: expired, httpOnly: true }) - ctx.cookies.set('user_id', user._id, { domain: cookie.domain, maxAge: expired }) - ctx.success({ - id: user._id, - token - }, 'login success') - } else { - ctx.fail(-1, 'incorrect password') - } - } else { - ctx.fail(-1, 'user doesn\'t exist') - } -} - -exports.logout = async (ctx, next) => { - const { expired, cookie } = config.auth - const token = signUserToken({ - id: ctx._user._id, - name: ctx._user.name - }, false) - ctx.cookies.set(cookie.name, token, { - maxAge: 0 - }) - ctx.success() -} - exports.update = async (ctx, next) => { const name = ctx.validateBody('name').optional().isString('the "name" parameter should be String type').val() const password = ctx.validateBody('password').optional().isString('the "password" parameter should be String type').val() @@ -166,13 +113,3 @@ exports.delete = async (ctx, next) => { ctx.fail() } } - -/** - * @desc jwt sign - * @param {Object} payload={} - * @param {Boolean} isLogin=false - */ -function signUserToken (payload = {}, isLogin = true) { - const { secretKey, expired } = config.auth - return jwt.sign(payload, secretKey, { expiresIn: isLogin ? expired : 0 }) -} diff --git a/server/middleware/auth.js b/server/middleware/auth.js deleted file mode 100644 index e766c95..0000000 --- a/server/middleware/auth.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @desc Auth middleware - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const compose = require('koa-compose') -const koajwt = require('koa-jwt') -const jwt = require('jsonwebtoken') -const config = require('../config') -const { UserModel } = require('../model') -const isProd = process.env.NODE_ENV === 'production' - -// 开发环境下,请求携带_DEV_参数,视为已验证 -function devAuth () { - return async (ctx, next) => { - if (!isProd && ctx.query._DEV_) { - ctx._devauth = true - } - await next() - } -} - -function verifyToken () { - return async (ctx, next) => { - if (ctx._devauth) { - return await next() - } - const token = ctx.cookies.get(config.auth.cookie.name, { signed: true }) - if (token) { - try { - const decodedToken = await jwt.verify(token, config.auth.secretKey) - if (decodedToken.exp > Math.floor(Date.now() / 1000)) { - // 已验证权限 - await next() - } - } catch (err) { - ctx.fail(401, err.message) - } - } - ctx.fail(401) - } -} - -exports.isAuthenticated = () => { - return compose([ - devAuth(), - verifyToken(), - async (ctx, next) => { - if (ctx._devauth) { - return await next() - } else if (!ctx.state.user) { - ctx.fail(401) - return - } - await next() - }, - async (ctx, next) => { - if (ctx._devauth) { - ctx._isAuthenticated = true - return await next() - } - const userId = ctx.cookies.get('user_id', { domain: config.cookie.domain }) - const user = await UserModel.findById(userId).exec().catch(err => { - ctx.log.error(err.message) - return null - }) - if (!user) { - ctx.fail(401) - return - } - ctx._user = user - ctx._isAuthenticated = true - await next() - } - ]) -} diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js new file mode 100644 index 0000000..a5ebad2 --- /dev/null +++ b/server/middleware/authenticate.js @@ -0,0 +1,98 @@ +/** + * @desc Auth middleware + * @author Jooger + * @date 25 Sep 2017 + */ + +'use strict' + +const compose = require('koa-compose') +const koajwt = require('koa-jwt') +const jwt = require('jsonwebtoken') +const passport = require('koa-passport') +const config = require('../config') +const { UserModel } = require('../model') +const isProd = process.env.NODE_ENV === 'production' + +// 开发环境下,请求携带_DEV_参数,视为已验证 +function devAuth () { + return async (ctx, next) => { + if (!isProd && ctx.query._DEV_) { + ctx.session._verify = true + } + await next() + } +} + +function verifyToken () { + return async (ctx, next) => { + if (ctx._devauth) { + return await next() + } + ctx.session._verify = false + const token = ctx.cookies.get(config.auth.session.key) + if (token) { + const decodedToken = await jwt.verify(token, config.auth.secrets) + if (decodedToken.exp > Math.floor(Date.now() / 1000)) { + // 已验证权限 + ctx.session._verify = true + } + } + await next() + } +} + +exports.isAuthenticated = () => { + return compose([ + devAuth(), + verifyToken(), + async (ctx, next) => { + if (!ctx.session._verify) { + return ctx.fail(401) + } + + const userId = ctx.cookies.get('user_id', { signed: false }) + + const user = await UserModel.findById(userId).exec().catch(err => { + ctx.log.error(err.message) + return null + }) + if (!user) { + return ctx.fail(401, 'the user was not found') + } + ctx._user = user + ctx._isAuthenticated = true + await next() + } + ]) +} + +exports.snsAuth = (name = '') => { + return compose([ + verifyToken(), + async (ctx, next) => { + // 如果已经登录 + if (ctx.session._verify) { + return ctx.fail(-1, 'you have already logged in') + } + ctx.session.passport = { + redirectUrl: ctx.query.redirectUrl || '/' + } + await next() + }, + passport.authenticate(name, { + failureRedirect: '/', + session: false + }) + ]) +} + +exports.snsLogout = () => compose([ + verifyToken(), + async (ctx, next) => { + if (!ctx.session._verify) { + return ctx.fail(-1, 'please login first') + } + await next() + } +]) diff --git a/server/middleware/index.js b/server/middleware/index.js index 2a64296..56e4875 100644 --- a/server/middleware/index.js +++ b/server/middleware/index.js @@ -8,5 +8,5 @@ exports.error = require('./error') exports.response = require('./response') -exports.auth = require('./auth') +exports.authenticate = require('./authenticate') exports.header = require('./header') diff --git a/server/model/schema/user.js b/server/model/schema/user.js index 2a0031e..92fa799 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -14,14 +14,21 @@ const userSchema = new mongoose.Schema({ password: { type: String, default: '' - // default: md5(`${config.auth.secretKey}${config.auth.defaultPassword}`) }, slogan: { type: String, default: '' }, - avatar: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, + avatar: { type: String, default: '' }, // 角色 0 管理员 | 1 普通用户 role: { type: Number, default: 1 }, createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now } + updatedAt: { type: Date, default: Date.now }, + github: { + id: { type: String, default: '' }, + email: { type: String, default: '' }, + login: { type: String, default: '' }, + name: { type: String, default: '' }, + blog: { type: String, default: '' }, + token: { type: String, default: '' } + } }) module.exports = userSchema diff --git a/server/mongo.js b/server/mongo.js index 8cc186d..8c111db 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -9,13 +9,14 @@ const mongoose = require('mongoose') const config = require('./config') const { UserModel, OptionModel } = require('./model') -const { debug, bhash } = require('./util') +const { bhash, setDebug } = require('./util') +const debug = setDebug('mongo:connect') module.exports = function () { mongoose.Promise = global.Promise mongoose.connect(config.mongo.uri, config.mongo.option, err => { if (err) { - console.error('connect to %s error: ', config.mongo.uri, err.message) + debug('connect to %s error: ', config.mongo.uri, err.message) process.exit(0) } }) diff --git a/server/routes/backend.js b/server/routes/backend.js index 19695c7..66c48a8 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -7,9 +7,9 @@ 'use strict' const router = require('koa-router')() -const { article, tag, option, user } = require('../controller') -const { auth } = require('../middleware') -const isAuthenticated = auth.isAuthenticated() +const { article, tag, option, user, auth } = require('../controller') +const { authenticate } = require('../middleware') +const isAuthenticated = authenticate.isAuthenticated() // Article router.get('/articles', isAuthenticated, article.list) @@ -32,10 +32,12 @@ router.patch('/options', isAuthenticated, option.update) // User router.get('/users', isAuthenticated, user.list) -router.post('/users/login', user.login) -router.get('/users/logout', isAuthenticated, user.logout) router.get('/users/:id', isAuthenticated, user.item) router.patch('/users/:id', isAuthenticated, user.update) router.delete('/users/:id', isAuthenticated, user.delete) +// Auth +router.get('/auth/local/logout', isAuthenticated, auth.logout) +router.post('/auth/local/login', auth.localLogin) + module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 69efe39..74ea3ba 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -7,7 +7,11 @@ 'use strict' const router = require('koa-router')() -const { article, tag, music, option, user } = require('../controller') +const { article, tag, music, option, user, auth } = require('../controller') +const { authenticate } = require('../middleware') +const isAuthenticated = authenticate.isAuthenticated() +const snsAuth = authenticate.snsAuth +const snsLogout = authenticate.snsLogout() // Article router.get('/articles', article.list) @@ -31,4 +35,9 @@ router.get('/options', option.data) // User router.get('/users/:id', user.item) +// Auth +router.get('/auth/logout', isAuthenticated, auth.logout) +router.get('/auth/github/login', snsAuth('github')) + .get('/callback', auth.githubLogin) + module.exports = router diff --git a/server/service/github-passport.js b/server/service/github-passport.js new file mode 100644 index 0000000..9d39b78 --- /dev/null +++ b/server/service/github-passport.js @@ -0,0 +1,84 @@ +/** + * @desc github password service + * @author Jooger + * @date 27 Sep 2017 + */ + +'use strict' + +const passport = require('koa-passport') +const GithubStrategy = require('passport-github').Strategy +const config = require('../config') +const { clientID, clientSecret, callbackURL } = config.sns.github +const { randomString, setDebug } = require('../util') +const debug = setDebug('auth:github') + +exports.init = (UserModel, config) => { + passport.use(new GithubStrategy({ + clientID, + clientSecret, + callbackURL, + passReqToCallback: true + }, async (req, accessToken, refreshToken, profile, done) => { + debug('github auth start') + try { + const user = await UserModel.findOne({ + 'github.id': profile.id + }).catch(err => { + debug('user check error, err: ', err.message) + return null + }) + + + if (user) { + const userData = { + name: profile.displayName || profile.username, + avatar: profile._json.avatar_url, + slogan: profile._json.bio, + github: profile._json, + role: user.role + } + + const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData).exec().catch(err => { + debug('user update error, err: ', err.message) + }) || user + + return end(null, updatedUser) + } + + const newUser = { + name: profile.displayName || profile.username, + avatar: profile._json.avatar_url, + slogan: profile._json.bio, + github: profile._json, + role: 1 + } + + newUser.github.token = accessToken + + const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { + debug('user check error, err: ', err.message) + return true + }) + + if (checkUser) { + newUser.name += '-' + randomString() + } + + const data = await new UserModel(newUser).save().catch(err => { + debug('user create fail, err: ', err.message) + }) + + return end(null, data) + } catch (err) { + debug('github auth error') + return end(err) + } + + function end (err, data) { + debug('github auth finish') + done(err, data) + } + })) +} + diff --git a/server/service/index.js b/server/service/index.js new file mode 100644 index 0000000..1a86540 --- /dev/null +++ b/server/service/index.js @@ -0,0 +1,9 @@ +/** + * @desc Services entry + * @author Jooger + * @date 27 Sep 2017 + */ + +'use strict' + +exports.githubPassport = require('./github-passport') diff --git a/server/util/index.js b/server/util/index.js index e1c519c..047eb48 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -8,11 +8,16 @@ const bcrypt = require('bcryptjs') const mongoose = require('mongoose') -const debug = require('debug')(require('../../package.json').name) +const packageInfo = require('../../package.json') +const debug = require('debug') -debug.enabled = true +exports.setDebug = level => { + const deBug = debug(`[${packageInfo.name}]${level ? ' ' + level : ''}`) + deBug.enabled = true + return deBug +} -exports.debug = debug +exports.signToken = require('./sign-token') exports.marked = require('./marked') @@ -26,3 +31,12 @@ exports.firstUpperCase = (str = '') => str.toLowerCase().replace(/( |^)[a-z]/g, exports.bhash = (str = '') => bcrypt.hashSync(str, 8) exports.bcompare = (str, hash) => bcrypt.compareSync(str, hash) + +exports.randomString = (length = 8) => { + const chars = `ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz` + let id = '' + for (let i = 0; i < length; i++) { + id += chars[Math.floor(Math.random() * chars.length)] + } + return id +} diff --git a/server/util/sign-token.js b/server/util/sign-token.js new file mode 100644 index 0000000..5dc1208 --- /dev/null +++ b/server/util/sign-token.js @@ -0,0 +1,15 @@ +/** + * @desc jwt sign token + * @author Jooger + * @date 27 Sep 2017 + */ + +'use strict' + +const jwt = require('jsonwebtoken') +const config = require('../config') + +module.exports = (payload = {}, isLogin = true) => { + const { secrets, session } = config.auth + return jwt.sign(payload, secrets, { expiresIn: isLogin ? session.maxAge : 0 }) +} diff --git a/server/validation.js b/server/validation.js index 338797c..c6037f1 100644 --- a/server/validation.js +++ b/server/validation.js @@ -33,7 +33,7 @@ Validator.addMethod('isObjectIdArray', function (tip) { const val = this.val() if (val !== undefined) { this.isArray() - val.every(data => { + val.forEach(data => { if (!isObjectId(data)) { this.throwError(tip || `the "${this.key}" parameter contains a data(${data}) that is not ObjectId type`) } From 02215a2fc3ff8fce6199e8d7652f37806b3d83cd Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 27 Sep 2017 15:41:16 +0800 Subject: [PATCH 017/208] [update] update debug --- bin/www | 2 -- server/controller/auth.js | 6 +++--- server/mongo.js | 6 +++--- server/service/github-passport.js | 10 ++++----- server/util/debug.js | 35 +++++++++++++++++++++++++++++++ server/util/index.js | 11 ++++------ 6 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 server/util/debug.js diff --git a/bin/www b/bin/www index 6e360c0..2a9184b 100755 --- a/bin/www +++ b/bin/www @@ -9,8 +9,6 @@ const app = require('../server/app') const debug = require('../server/util').setDebug() const config = require('../server/config') -debug.enabled = true - /** * Get port from environment and store in Express. */ diff --git a/server/controller/auth.js b/server/controller/auth.js index 3583e3e..114e7e7 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -44,7 +44,7 @@ exports.localLogin = async (ctx, next) => { }) ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: true }) ctx.cookies.set('user_id', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - debug('login success, user: ', user._id) + debug.success('login success, user: ', user._id) ctx.success({ id: user._id, token @@ -65,7 +65,7 @@ exports.logout = async (ctx, next) => { }, false) ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: 0, httpOnly: true }) ctx.cookies.set('user_id', ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) - debug('logout success, user: ', ctx._user._id) + debug.success('logout success, user: ', ctx._user._id) ctx.success(null, 'logout success') } @@ -87,7 +87,7 @@ exports.githubLogin = async (ctx, next) => { ctx.cookies.set('user_id', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) debug('github auth callback finish') - debug('github login success, user: ', user._id) + debug.success('github login success, user: ', user._id) return ctx.redirect(redirectUrl) })(ctx) } diff --git a/server/mongo.js b/server/mongo.js index 8c111db..358d85d 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -16,7 +16,7 @@ module.exports = function () { mongoose.Promise = global.Promise mongoose.connect(config.mongo.uri, config.mongo.option, err => { if (err) { - debug('connect to %s error: ', config.mongo.uri, err.message) + debug.error('connect to %s error: ', config.mongo.uri, err.message) process.exit(0) } }) @@ -33,7 +33,7 @@ function seedOption () { }) function createOption () { - new OptionModel().save().catch(err => debug(err.message)) + new OptionModel().save().catch(err => debug.error(err.message)) } } @@ -50,6 +50,6 @@ function seedAdmin () { password: bhash(config.auth.defaultPassword) }) .save() - .catch(err => debug(err.message)) + .catch(err => debug.error(err.message)) } } diff --git a/server/service/github-passport.js b/server/service/github-passport.js index 9d39b78..ff0c0c3 100644 --- a/server/service/github-passport.js +++ b/server/service/github-passport.js @@ -25,7 +25,7 @@ exports.init = (UserModel, config) => { const user = await UserModel.findOne({ 'github.id': profile.id }).catch(err => { - debug('user check error, err: ', err.message) + debug.error('user check error, err: ', err.message) return null }) @@ -40,7 +40,7 @@ exports.init = (UserModel, config) => { } const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData).exec().catch(err => { - debug('user update error, err: ', err.message) + debug.error('user update error, err: ', err.message) }) || user return end(null, updatedUser) @@ -57,7 +57,7 @@ exports.init = (UserModel, config) => { newUser.github.token = accessToken const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { - debug('user check error, err: ', err.message) + debug.error('user check error, err: ', err.message) return true }) @@ -66,12 +66,12 @@ exports.init = (UserModel, config) => { } const data = await new UserModel(newUser).save().catch(err => { - debug('user create fail, err: ', err.message) + debug.error('user create fail, err: ', err.message) }) return end(null, data) } catch (err) { - debug('github auth error') + debug.error('github auth error') return end(err) } diff --git a/server/util/debug.js b/server/util/debug.js new file mode 100644 index 0000000..b87afc9 --- /dev/null +++ b/server/util/debug.js @@ -0,0 +1,35 @@ +/** + * @desc Debug wrapper for debug + * @author Jooger + * @date 27 Sep 2017 + */ + +'use strict' + +const debug = require('debug') +const packageInfo = require('../../package.json') + +const levelMap = { + success: 2, + info: 6, + warn: 3, + error: 1 +} + +module.exports = function setDebug (namespace = '') { + const deBug = debug(`[${packageInfo.name}]${namespace ? ' ' + namespace : ''}`) + + function d () { + d.info.apply(d, Array.prototype.slice.call(arguments)) + } + + Object.keys(levelMap).map(level => { + d[level] = function () { + deBug.enabled = true + deBug.color = levelMap[level] + deBug.apply(null, Array.prototype.slice.call(arguments)) + } + }) + + return d +} diff --git a/server/util/index.js b/server/util/index.js index 047eb48..4e0f620 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -8,14 +8,8 @@ const bcrypt = require('bcryptjs') const mongoose = require('mongoose') -const packageInfo = require('../../package.json') -const debug = require('debug') -exports.setDebug = level => { - const deBug = debug(`[${packageInfo.name}]${level ? ' ' + level : ''}`) - deBug.enabled = true - return deBug -} +exports.setDebug = require('./debug') exports.signToken = require('./sign-token') @@ -28,10 +22,13 @@ exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) // 首字母大写 exports.firstUpperCase = (str = '') => str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) +// hash 加密 exports.bhash = (str = '') => bcrypt.hashSync(str, 8) +// 对比 exports.bcompare = (str, hash) => bcrypt.compareSync(str, hash) +// 随机字符串 exports.randomString = (length = 8) => { const chars = `ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz` let id = '' From d07afee91a74894f17b242036324d08ba7ec25d2 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 27 Sep 2017 17:43:16 +0800 Subject: [PATCH 018/208] [update] update option api, add crontab to update option per 3 hour --- package-lock.json | 20 +++++++++++++-- package.json | 1 + server/config/index.js | 5 ++-- server/controller/option.js | 42 ++++++++++++++++++++++++++++--- server/controller/user.js | 18 +++++++++++++ server/middleware/authenticate.js | 1 + server/model/schema/option.js | 2 ++ server/routes/backend.js | 2 +- server/routes/frontend.js | 3 ++- server/service/github-userinfo.js | 40 +++++++++++++++++++++++++++++ server/service/index.js | 2 ++ 11 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 server/service/github-userinfo.js diff --git a/package-lock.json b/package-lock.json index 2f2e25e..b71b8fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,6 +90,15 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "axios": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", + "integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=", + "requires": { + "follow-redirects": "1.2.4", + "is-buffer": "1.1.5" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -591,6 +600,14 @@ "repeat-string": "1.6.1" } }, + "follow-redirects": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.2.4.tgz", + "integrity": "sha512-Suw6KewLV2hReSyEOeql+UUkBVyiBm3ok1VPrVFRZnQInWpdoZbbiG5i8aJVSjTr0yQ4Ava0Sh6/joCg1Brdqw==", + "requires": { + "debug": "2.6.9" + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -1694,8 +1711,7 @@ "is-buffer": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", - "dev": true + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" }, "is-class": { "version": "0.0.4", diff --git a/package.json b/package.json index 7131c52..9559fe3 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ }, "bin": "./node_modules/.bin/", "dependencies": { + "axios": "^0.16.2", "bcryptjs": "^2.4.3", "debug": "^2.6.9", "highlight.js": "^9.12.0", diff --git a/server/config/index.js b/server/config/index.js index 1697f13..3f16a76 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -13,6 +13,7 @@ const packageInfo = require('../../package.json') const baseConfig = { name: packageInfo.name, version: packageInfo.version, + author: packageInfo.author || 'Jooger', env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3001, @@ -40,8 +41,8 @@ const baseConfig = { signed: false }, secrets: `${packageInfo.name} ${packageInfo.version}`, - defaultName: 'admin', - defaultPassword: 'admin', + defaultName: 'Jooger', + defaultPassword: 'admin_jooger', // 允许请求的域名 allowedOrigins: [ 'jooger.me', diff --git a/server/controller/option.js b/server/controller/option.js index fd57bc8..856ad85 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -7,6 +7,7 @@ 'use strict' const { OptionModel } = require('../model') +const { getGithubUsersInfo } = require('../service') exports.data = async (ctx, next) => { const data = await OptionModel.findOne().exec().catch(err => { @@ -24,10 +25,7 @@ exports.data = async (ctx, next) => { exports.update = async (ctx, next) => { const option = ctx.request.body - const data = await OptionModel.findOneAndUpdate({}, option, { new: true }).exec().catch(err => { - ctx.log.error(err.message) - return null - }) + const data = await updateOption(option) if (data) { ctx.success(data) @@ -35,3 +33,39 @@ exports.update = async (ctx, next) => { ctx.fail() } } + +// 每3小时更新一次 +setInterval(updateOption, 1000 * 60 * 60 * 3) + +async function updateOption (option = null) { + if (!option) { + option = await OptionModel.findOne().exec().catch(err => { + ctx.log.error(err.message) + return {} + }) + } + // 更新友链 + if (option.links) { + const githubNames = option.links.map(link => link.github) + const usersInfo = await getGithubUsersInfo(githubNames) + + if (usersInfo) { + option.links = option.links.map((link, index) => { + const userInfo = usersInfo[index] + if (userInfo) { + link.avatar = userInfo.avatar_url + link.slogan = userInfo.bio + link.site = link.site || userInfo.blog + } + return link + }) + } + } + + const data = await OptionModel.findOneAndUpdate({}, option, { new: true }).exec().catch(err => { + ctx.log.error(err.message) + return null + }) + + return data +} diff --git a/server/controller/user.js b/server/controller/user.js index da0df67..42580a6 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -8,6 +8,7 @@ const { UserModel } = require('../model') const { bhash, bcompare } = require('../util') +const config = require('../config') exports.list = async (ctx, next) => { let select = '-password' @@ -113,3 +114,20 @@ exports.delete = async (ctx, next) => { ctx.fail() } } + +exports.me = async (ctx, next) => { + const data = await UserModel + .findOne({ name: config.author }) + .select('-password -role -createdAt -updatedAt -github') + .exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index a5ebad2..fdf34ea 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -31,6 +31,7 @@ function verifyToken () { } ctx.session._verify = false const token = ctx.cookies.get(config.auth.session.key) + if (token) { const decodedToken = await jwt.verify(token, config.auth.secrets) if (decodedToken.exp > Math.floor(Date.now() / 1000)) { diff --git a/server/model/schema/option.js b/server/model/schema/option.js index 351df16..6e2a2b5 100644 --- a/server/model/schema/option.js +++ b/server/model/schema/option.js @@ -38,6 +38,8 @@ const optionSchema = new mongoose.Schema({ links: [{ name: { type: String, required: true }, github: { type: String, default: '' }, + avatar: { type: String, default: '' }, + slogan: { type: String, default: '' }, site: { type: String, required: true } }], musicId: { type: String, default: '' } diff --git a/server/routes/backend.js b/server/routes/backend.js index 66c48a8..e95bc3e 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -17,7 +17,7 @@ router.get('/articles/:id', isAuthenticated, article.item) router.post('/articles', isAuthenticated, article.create) router.patch('/articles/:id', isAuthenticated, article.update) router.delete('/articles/:id', isAuthenticated, article.delete) -router.get('/articles/:id/like', isAuthenticated, article.like) +router.post('/articles/:id/like', isAuthenticated, article.like) // Tag router.get('/tags', isAuthenticated, tag.list) diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 74ea3ba..aee259b 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -16,7 +16,7 @@ const snsLogout = authenticate.snsLogout() // Article router.get('/articles', article.list) router.get('/articles/:id', article.item) -router.get('/articles/:id/like', article.like) +router.post('/articles/:id/like', article.like) // Tag router.get('/tags', tag.list) @@ -33,6 +33,7 @@ router.get('/music/songs/cover/:cover_id', music.cover) router.get('/options', option.data) // User +router.get('/users/me', user.me) router.get('/users/:id', user.item) // Auth diff --git a/server/service/github-userinfo.js b/server/service/github-userinfo.js new file mode 100644 index 0000000..c4e2b73 --- /dev/null +++ b/server/service/github-userinfo.js @@ -0,0 +1,40 @@ +/** + * @desc github userinfo fetch service + * @author Jooger + * @date 27 Sep 2017 + */ + +'use strict' + +const axios = require('axios') +const { setDebug } = require('../util') +const debug = setDebug('github:user') + +const getGithubUsersInfo = (githubNames = '') => { + if (!githubNames) { + return null + } else if (typeof githubNames === 'string') { + githubNames = [githubNames] + } else if (!Array.isArray(githubNames)) { + return null + } + + const task = githubNames.map(name => { + debug('fetch github user ', name) + return axios.get(`https://api.github.com/users/${name}`) + .then(res => { + if (res && res.status === 200) { + return res.data + } + return null + }) + .catch(err => { + debug.error(err.message) + return null + }) + }) + + return Promise.all(task) +} + +module.exports = getGithubUsersInfo diff --git a/server/service/index.js b/server/service/index.js index 1a86540..f9a2a6c 100644 --- a/server/service/index.js +++ b/server/service/index.js @@ -7,3 +7,5 @@ 'use strict' exports.githubPassport = require('./github-passport') + +exports.getGithubUsersInfo = require('./github-userinfo') From 95f8917442b17037a338ba48d74dc12510897dc6 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 27 Sep 2017 18:43:59 +0800 Subject: [PATCH 019/208] [update] update header middleware and pm2 process file --- ecosystem.config.js | 6 +++--- server/middleware/header.js | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ecosystem.config.js b/ecosystem.config.js index 9a6b5bf..8ac5b74 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -14,14 +14,14 @@ module.exports = { script: './bin/www', cwd: __dirname, watch: true, - ignore_watch: ["[\/\\]\./", "node_modules"], + ignore_watch: ['[\/\\]\./', 'node_modules'], env: { NODE_ENV: 'production' }, env_production: { - NODE_ENV: "production" + NODE_ENV: 'production' }, - log_date_format: "YYYY-MM-DD HH:mm Z", + log_date_format: 'YYYY-MM-DD HH:mm Z', out_file: './logs/pm2-out.log', error_file: './logs/pm2-error.log', pid_file: './logs/jooger.me-server.pid' diff --git a/server/middleware/header.js b/server/middleware/header.js index 938c210..d4eb2d0 100644 --- a/server/middleware/header.js +++ b/server/middleware/header.js @@ -12,7 +12,10 @@ module.exports = async (ctx, next) => { const { request, response } = ctx const allowedOrigins = config.auth.allowedOrigins const origin = request.get('origin') || '' - const allowed = origin.includes('localhost') || request.query._DEV_ || allowedOrigins.find(item => origin.includes(item)) + const allowed = request.query._DEV_ || + origin.includes('localhost') || + origin.includes('127.0.0.1') || + allowedOrigins.find(item => origin.includes(item)) if (allowed) { response.set('Access-Control-Allow-Origin', origin) } From eff2ca13ae87544ca5b046f16f66ca6f0ce662da Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 28 Sep 2017 23:49:44 +0800 Subject: [PATCH 020/208] [update] update music api --- server/controller/music.js | 29 +++++++++++++++++++++++++---- server/routes/frontend.js | 4 ++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index 7c55731..c8dbc29 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -17,9 +17,30 @@ exports.list = async (ctx, next) => { .isString('the "play_list_id" parameter should be String type') .val() - const { playlist } = await neteaseMusic.playlist(playListId) - - ctx.success(playlist) + const tracks = await neteaseMusic.playlist(playListId).then(({ playlist }) => playlist.tracks) + + const data = await Promise.all( + tracks.map(track => { + return Promise.all([ + neteaseMusic.url(track.id), + neteaseMusic.lyric(track.id) + ]).then(([song, lyric]) => { + return [song.data[0] || null, lyric.nolyric ? '' : lyric.lrc.lyric] + }).then(([song, lyric]) => { + const { id, name, dt, al, ar } = track + return { + id, + name, + duration: dt || 0, + album: al || {}, + artists: ar || [], + src: song.url, + lyric + } + }) + } + )) + ctx.success(data) } exports.item = async (ctx, next) => { @@ -47,7 +68,7 @@ exports.url = async (ctx, next) => { } exports.lyric = async (ctx, next) => { - const coverId = ctx.validateParam('song_id') + const songId = ctx.validateParam('song_id') .required('the "song_id" parameter is required') .notEmpty() .isString('the "song_id" parameter should be String type') diff --git a/server/routes/frontend.js b/server/routes/frontend.js index aee259b..f2c0185 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -25,8 +25,8 @@ router.get('/tags/:id', tag.item) // Music router.get('/music/songs', music.list) router.get('/music/songs/:song_id', music.item) -router.get('/music/songs/:song_id/url', music.url) -router.get('/music/songs/:song_id/lyric', music.lyric) +// router.get('/music/songs/:song_id/url', music.url) +// router.get('/music/songs/:song_id/lyric', music.lyric) router.get('/music/songs/cover/:cover_id', music.cover) // Option From 604fcc529eb80395c922ef1ba7571074f56f369a Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 30 Sep 2017 18:43:32 +0800 Subject: [PATCH 021/208] [update] update music api, add cache control --- package-lock.json | 10 +++ package.json | 2 + server/controller/music.js | 138 +++++++++++++++++++++++++------- server/controller/option.js | 11 ++- server/routes/frontend.js | 4 +- server/service/index.js | 2 + server/service/netease-music.js | 96 ++++++++++++++++++++++ server/util/debug.js | 5 +- server/util/encrypt.js | 61 ++++++++++++++ server/util/index.js | 2 + 10 files changed, 296 insertions(+), 35 deletions(-) create mode 100644 server/service/netease-music.js create mode 100644 server/util/encrypt.js diff --git a/package-lock.json b/package-lock.json index b71b8fe..fb751a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -122,6 +122,11 @@ "callsite": "1.0.0" } }, + "big-integer": { + "version": "1.6.25", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.25.tgz", + "integrity": "sha1-HeRan1dUKsIBIcaC+NZCIgo06CM=" + }, "binary-extensions": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", @@ -414,6 +419,11 @@ "which": "1.3.0" } }, + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" + }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", diff --git a/package.json b/package.json index 9559fe3..b014732 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "dependencies": { "axios": "^0.16.2", "bcryptjs": "^2.4.3", + "big-integer": "^1.6.25", + "crypto": "^1.0.1", "debug": "^2.6.9", "highlight.js": "^9.12.0", "koa": "^2.2.0", diff --git a/server/controller/music.js b/server/controller/music.js index c8dbc29..ea7a0d0 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -7,39 +7,37 @@ 'use strict' const NeteseMusic = require('simple-netease-cloud-music') +// const { fetchNE } = require('../service') +const { OptionModel } = require('../model') +const debug = require('../util').setDebug('music') const neteaseMusic = new NeteseMusic() +let songListMap = {} + exports.list = async (ctx, next) => { - const playListId = ctx.validateQuery('play_list_id') - .required('the "play_list_id" parameter is required') - .notEmpty() - .isString('the "play_list_id" parameter should be String type') - .val() - - const tracks = await neteaseMusic.playlist(playListId).then(({ playlist }) => playlist.tracks) - - const data = await Promise.all( - tracks.map(track => { - return Promise.all([ - neteaseMusic.url(track.id), - neteaseMusic.lyric(track.id) - ]).then(([song, lyric]) => { - return [song.data[0] || null, lyric.nolyric ? '' : lyric.lrc.lyric] - }).then(([song, lyric]) => { - const { id, name, dt, al, ar } = track - return { - id, - name, - duration: dt || 0, - album: al || {}, - artists: ar || [], - src: song.url, - lyric - } - }) - } - )) + // const playListId = ctx.validateQuery('play_list_id') + // .required('the "play_list_id" parameter is required') + // .notEmpty() + // .isString('the "play_list_id" parameter should be String type') + // .val() + + const option = await OptionModel.findOne({}).exec().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (!option) { + return ctx.fail() + } + + const musicId = option.musicId + if (songListMap[musicId]) { + return ctx.success(songListMap[musicId]) + } + + const data = await fetchSonglist(musicId) + songListMap[musicId] = data ctx.success(data) } @@ -90,3 +88,85 @@ exports.cover = async (ctx, next) => { ctx.success(data) } + +// TEST +// exports.test = async (ctx, next) => { +// const playListId = ctx.validateQuery('play_list_id') +// .required('the "play_list_id" parameter is required') +// .notEmpty() +// .isString('the "play_list_id" parameter should be String type') +// .val() +// const tracks = await fetchNE('playlist', playListId) +// .then(({ playlist }) => { +// return playlist.tracks.map(({ name, id, ar, al, dt, tns }) => ({ +// id, +// name, +// duration: dt, +// artists: ar.map(({ id, name }) => ({ id, name })), +// album: { +// name: al.name, +// cover: al.picUrl, +// tns: al.tns +// }, +// tns: tns || [] +// })) +// }) +// ctx.success(tracks) +// } + +async function fetchSonglist (playListId) { + return neteaseMusic.playlist(playListId).then(({ playlist }) => { + return Promise.all( + playlist.tracks.map(track => { + return Promise.all([ + neteaseMusic.url(track.id), + neteaseMusic.lyric(track.id) + ]) + .then(([song, lyric]) => [song.data[0] || null, lyric.nolyric ? '' : lyric.lrc.lyric]) + .then(([song, lyric]) => { + const { id, name, dt, al, ar } = track + return { + id, + name, + duration: dt || 0, + album: al || {}, + artists: ar || [], + src: song.url, + lyric + } + }) + } + )) + }) +} + +// 每1小时更新一次 +setInterval(updateSongListMap, 1000 * 60 * 60) +setTimeout(updateSongListMap, 0) + +// 更新song list cache +async function updateSongListMap () { + debug('timed update music...') + + const option = await OptionModel.findOne({}).exec().catch(err => { + debug.error(err.message) + return null + }) + + if (option && option.musicId) { + songListMap[option.musicId] = null + } + + const ids = Object.keys(songListMap) + const list = await Promise.all(ids.map(playListId => fetchSonglist(playListId))) + .catch(err => debug.error('timed update music failed, err: ', err.message)) + + if (list && list.length === ids.length) { + ids.map((id, index) => { + songListMap[id] = list[index] + }) + debug.success('timed update music success...') + } +} + +exports.updateSongListMap = updateSongListMap diff --git a/server/controller/option.js b/server/controller/option.js index 856ad85..5d37a84 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -8,6 +8,7 @@ const { OptionModel } = require('../model') const { getGithubUsersInfo } = require('../service') +const debug = require('../util').setDebug('option') exports.data = async (ctx, next) => { const data = await OptionModel.findOne().exec().catch(err => { @@ -34,10 +35,12 @@ exports.update = async (ctx, next) => { } } -// 每3小时更新一次 -setInterval(updateOption, 1000 * 60 * 60 * 3) +// 每1小时更新一次 +setInterval(updateOption, 1000 * 60 * 60 * 1) +setTimeout(updateOption, 0) async function updateOption (option = null) { + debug('timed update option...') if (!option) { option = await OptionModel.findOne().exec().catch(err => { ctx.log.error(err.message) @@ -67,5 +70,9 @@ async function updateOption (option = null) { return null }) + if (data) { + debug.success('timed update option success...') + } + return data } diff --git a/server/routes/frontend.js b/server/routes/frontend.js index f2c0185..f089c60 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -24,10 +24,10 @@ router.get('/tags/:id', tag.item) // Music router.get('/music/songs', music.list) -router.get('/music/songs/:song_id', music.item) +// router.get('/music/songs/:song_id', music.item) // router.get('/music/songs/:song_id/url', music.url) // router.get('/music/songs/:song_id/lyric', music.lyric) -router.get('/music/songs/cover/:cover_id', music.cover) +// router.get('/music/songs/cover/:cover_id', music.cover) // Option router.get('/options', option.data) diff --git a/server/service/index.js b/server/service/index.js index f9a2a6c..1d6b2d9 100644 --- a/server/service/index.js +++ b/server/service/index.js @@ -9,3 +9,5 @@ exports.githubPassport = require('./github-passport') exports.getGithubUsersInfo = require('./github-userinfo') + +exports.fetchNE = require('./netease-music') diff --git a/server/service/netease-music.js b/server/service/netease-music.js new file mode 100644 index 0000000..1a5a762 --- /dev/null +++ b/server/service/netease-music.js @@ -0,0 +1,96 @@ +/** + * @desc 网易云音乐 TEST + * @author Jooger + * @date 30 Sep 2017 + */ + +'use strict' + +const axios = require('axios') +const { encrypt, setDebug } = require('../util') +const debug = setDebug('netease') + +const neFetcher = axios.create({ + baseURL: 'http://music.163.com', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': '*/*', + 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', + 'Connection': 'keep-alive', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': 'http://music.163.com', + 'Host': 'music.163.com', + 'Cookie': 'appver=2.0.2;', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36', + } +}) + +const links = { + playlist: '/weapi/v3/playlist/detail', + song: '/weapi/v3/song/detail', + songUrl: '/weapi/song/enhance/player/url' +} + +const fetchNE = function (type = '', id = '') { + return new Promise((resolve, reject) => { + if (!id) { + return reject(new Error('no id detect')) + } + let data = {} + + switch (type) { + case 'playlist': + data = { + id: id, + offset: 0, + total: true, + limit: 1000, + n: 1000, + csrf_token: '' + } + break + case 'song': + data = { + c: JSON.stringify([{ id }]), + ids: `[${id}]`, + csrf_token: '' + } + break + case 'songUrl': + data = { + ids: [id], + br: 999000, + csrf_token: '' + } + break + case 'songlyric': + data = { + id, + os: 'linux', + lv: -1, + kv: -1, + tv: -1, + } + break + default: + return reject(new Error('no support type')) + break + } + + neFetcher.request({ + method: 'post', + url: links[type], + params: encrypt(data) + }).then(res => { + if (res && res.status === 200) { + resolve(res.data) + } else { + reject(new Error(res.statusText)) + } + }).catch(err => { + debug.error(err.message) + }) + }) +} + +module.exports = fetchNE diff --git a/server/util/debug.js b/server/util/debug.js index b87afc9..84957b5 100644 --- a/server/util/debug.js +++ b/server/util/debug.js @@ -15,19 +15,20 @@ const levelMap = { warn: 3, error: 1 } +const slice = Array.prototype.slice module.exports = function setDebug (namespace = '') { const deBug = debug(`[${packageInfo.name}]${namespace ? ' ' + namespace : ''}`) function d () { - d.info.apply(d, Array.prototype.slice.call(arguments)) + d.info.apply(d, slice.call(arguments)) } Object.keys(levelMap).map(level => { d[level] = function () { deBug.enabled = true deBug.color = levelMap[level] - deBug.apply(null, Array.prototype.slice.call(arguments)) + deBug.apply(null, slice.call(arguments)) } }) diff --git a/server/util/encrypt.js b/server/util/encrypt.js new file mode 100644 index 0000000..922f7c4 --- /dev/null +++ b/server/util/encrypt.js @@ -0,0 +1,61 @@ +/** + * @desc + * @author Jooger + * @date 30 Sep 2017 + */ + +'use strict' + +const crypto = require('crypto') +const bigInt = require('big-integer') +const modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' +const nonce = '0CoJUm6Qyw8W8jud' +const pubKey = '010001' + +const createSecretKey = (size) => { + const keys = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + let key = "" + for (let i = 0; i < size; i++) { + let pos = Math.random() * keys.length + pos = Math.floor(pos) + key = key + keys.charAt(pos) + } + return key +} + +const aesEncrypt = (text, secKey) => { + const _text = text + const lv = new Buffer('0102030405060708', "binary") + const _secKey = new Buffer(secKey, "binary") + const cipher = crypto.createCipheriv('AES-128-CBC', _secKey, lv) + let encrypted = cipher.update(_text, 'utf8', 'base64') + encrypted += cipher.final('base64') + return encrypted +} + +const zfill = (str, size) => { + while (str.length < size) str = "0" + str + return str +} + +const rsaEncrypt = (text, pubKey, modulus) => { + const _text = text.split('').reverse().join('') + const biText = bigInt(new Buffer(_text).toString('hex'), 16), + biEx = bigInt(pubKey, 16), + biMod = bigInt(modulus, 16), + biRet = biText.modPow(biEx, biMod) + return zfill(biRet.toString(16), 256) +} + +const encrypt = (params) => { + const text = JSON.stringify(params) + const secKey = createSecretKey(16) + const encText = aesEncrypt(aesEncrypt(text, nonce), secKey) + const encSecKey = rsaEncrypt(secKey, pubKey, modulus) + return { + params: encText, + encSecKey: encSecKey + } +} + +module.exports = encrypt diff --git a/server/util/index.js b/server/util/index.js index 4e0f620..4d35176 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -15,6 +15,8 @@ exports.signToken = require('./sign-token') exports.marked = require('./marked') +exports.encrypt = require('./encrypt') + exports.createObjectId = () => mongoose.Types.ObjectId() exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) From c4d67f650824fe167768a04c4c44c0c0c58b9168 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 30 Sep 2017 23:06:51 +0800 Subject: [PATCH 022/208] [fix] fix timed bug --- ecosystem.config.js | 2 +- server/controller/music.js | 9 +++------ server/controller/option.js | 9 ++++----- server/mongo.js | 17 +++++++++++------ server/service/github-userinfo.js | 3 ++- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/ecosystem.config.js b/ecosystem.config.js index 8ac5b74..a802522 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -33,7 +33,7 @@ module.exports = { ref : 'origin/master', repo : packageInfo.repository.url, path : '/root/www/' + packageInfo.name, - 'post-deploy' : 'cnpm install && pm2 stop all && pm2 reload ecosystem.config.js --env production && pm2 start all' + 'post-deploy' : 'git pull && cnpm install && pm2 stop all && pm2 reload ecosystem.config.js --env production && pm2 start all' } } } diff --git a/server/controller/music.js b/server/controller/music.js index ea7a0d0..fcd41da 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -140,12 +140,8 @@ async function fetchSonglist (playListId) { }) } -// 每1小时更新一次 -setInterval(updateSongListMap, 1000 * 60 * 60) -setTimeout(updateSongListMap, 0) - // 更新song list cache -async function updateSongListMap () { +exports.updateSongListMap = async function () { debug('timed update music...') const option = await OptionModel.findOne({}).exec().catch(err => { @@ -169,4 +165,5 @@ async function updateSongListMap () { } } -exports.updateSongListMap = updateSongListMap +// 每1小时更新一次 +setInterval(exports.updateSongListMap, 1000 * 60 * 60) diff --git a/server/controller/option.js b/server/controller/option.js index 5d37a84..6642330 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -35,11 +35,7 @@ exports.update = async (ctx, next) => { } } -// 每1小时更新一次 -setInterval(updateOption, 1000 * 60 * 60 * 1) -setTimeout(updateOption, 0) - -async function updateOption (option = null) { +exports.updateOption = async function (option = null) { debug('timed update option...') if (!option) { option = await OptionModel.findOne().exec().catch(err => { @@ -76,3 +72,6 @@ async function updateOption (option = null) { return data } + +// 每1小时更新一次 +setInterval(exports.updateOption, 1000 * 60 * 60 * 1) diff --git a/server/mongo.js b/server/mongo.js index 358d85d..a8b7dac 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -9,6 +9,8 @@ const mongoose = require('mongoose') const config = require('./config') const { UserModel, OptionModel } = require('./model') +const { updateOption } = require('./controller/option') +const { updateSongListMap } = require('./controller/music') const { bhash, setDebug } = require('./util') const debug = setDebug('mongo:connect') @@ -25,12 +27,15 @@ module.exports = function () { seedAdmin() } -function seedOption () { - OptionModel.findOne().exec().then(data => { - if (!data) { - createOption() - } - }) +async function seedOption () { + let option = await OptionModel.findOne().exec().catch(err => debug.error(err.message)) + + if (!option) { + option = await createOption() + } + + updateOption(option) + updateSongListMap() function createOption () { new OptionModel().save().catch(err => debug.error(err.message)) diff --git a/server/service/github-userinfo.js b/server/service/github-userinfo.js index c4e2b73..64d45d5 100644 --- a/server/service/github-userinfo.js +++ b/server/service/github-userinfo.js @@ -20,10 +20,11 @@ const getGithubUsersInfo = (githubNames = '') => { } const task = githubNames.map(name => { - debug('fetch github user ', name) + debug('fetch github user [', name, ']') return axios.get(`https://api.github.com/users/${name}`) .then(res => { if (res && res.status === 200) { + debug.success('fetch github user success [', name, ']') return res.data } return null From 442ac18576a053d618f2b894330fa3449b763f79 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 30 Sep 2017 23:14:30 +0800 Subject: [PATCH 023/208] [update] update pm2 deploy --- ecosystem.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecosystem.config.js b/ecosystem.config.js index a802522..e15bd2c 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -33,7 +33,7 @@ module.exports = { ref : 'origin/master', repo : packageInfo.repository.url, path : '/root/www/' + packageInfo.name, - 'post-deploy' : 'git pull && cnpm install && pm2 stop all && pm2 reload ecosystem.config.js --env production && pm2 start all' + 'post-deploy' : 'git pull && npm install && pm2 stop all && pm2 reload ecosystem.config.js && pm2 start all' } } } From 4c4ec6be60d83af4ba610aa681dc9d01f96b575e Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 30 Sep 2017 23:18:28 +0800 Subject: [PATCH 024/208] [update] update pm2 deploy process file --- ecosystem.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecosystem.config.js b/ecosystem.config.js index e15bd2c..9630555 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -33,7 +33,7 @@ module.exports = { ref : 'origin/master', repo : packageInfo.repository.url, path : '/root/www/' + packageInfo.name, - 'post-deploy' : 'git pull && npm install && pm2 stop all && pm2 reload ecosystem.config.js && pm2 start all' + 'post-deploy' : 'git pull && cnpm install && pm2 stop all && pm2 reload ecosystem.config.js && pm2 start all' } } } From 808abb6b17c5b45873576fa52b93e88f9b82c5cd Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 30 Sep 2017 23:28:43 +0800 Subject: [PATCH 025/208] [fix] fix timed bug --- ecosystem.config.js | 2 +- server/controller/option.js | 1 + server/mongo.js | 10 +++------- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/ecosystem.config.js b/ecosystem.config.js index 9630555..54b0a5e 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -33,7 +33,7 @@ module.exports = { ref : 'origin/master', repo : packageInfo.repository.url, path : '/root/www/' + packageInfo.name, - 'post-deploy' : 'git pull && cnpm install && pm2 stop all && pm2 reload ecosystem.config.js && pm2 start all' + 'post-deploy' : 'git pull && cnpm install && pm2 stop all && pm2 startOrReload ecosystem.config.js && pm2 start all' } } } diff --git a/server/controller/option.js b/server/controller/option.js index 6642330..fca9c03 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -43,6 +43,7 @@ exports.updateOption = async function (option = null) { return {} }) } + // 更新友链 if (option.links) { const githubNames = option.links.map(link => link.github) diff --git a/server/mongo.js b/server/mongo.js index a8b7dac..e06eb4a 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -28,18 +28,14 @@ module.exports = function () { } async function seedOption () { - let option = await OptionModel.findOne().exec().catch(err => debug.error(err.message)) + const option = await OptionModel.findOne().exec().catch(err => debug.error(err.message)) if (!option) { - option = await createOption() + await new OptionModel().save().catch(err => debug.error(err.message)) } - updateOption(option) + updateOption() updateSongListMap() - - function createOption () { - new OptionModel().save().catch(err => debug.error(err.message)) - } } function seedAdmin () { From 21867c4c4be01ddb7291c66af9d7ade5ce1096bf Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 30 Sep 2017 23:46:23 +0800 Subject: [PATCH 026/208] [fix] fix bug --- server/controller/music.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/controller/music.js b/server/controller/music.js index fcd41da..c74413f 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -27,11 +27,12 @@ exports.list = async (ctx, next) => { return null }) - if (!option) { + if (!option || !option.musicId) { return ctx.fail() } const musicId = option.musicId + if (songListMap[musicId]) { return ctx.success(songListMap[musicId]) } @@ -151,6 +152,9 @@ exports.updateSongListMap = async function () { if (option && option.musicId) { songListMap[option.musicId] = null + } else { + debug('music playlist id is not found') + return } const ids = Object.keys(songListMap) From 0b7ba7b7ab874420aba5a06f5afb45b84d279bf0 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 10 Oct 2017 01:44:39 +0800 Subject: [PATCH 027/208] [fix] fix bugs --- server/config/index.js | 2 +- server/controller/article.js | 20 ++++++++------ server/controller/auth.js | 31 ++++++++++++++++++--- server/controller/music.js | 45 +++++++++---------------------- server/controller/user.js | 1 + server/middleware/authenticate.js | 15 ++++++++--- server/routes/backend.js | 1 + 7 files changed, 65 insertions(+), 50 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index 3f16a76..0611f09 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -36,7 +36,7 @@ const baseConfig = { redis: {}, auth: { session: { - key: 'jooger.me.sid', + key: 'jooger.me.token', maxAge: 60000 * 60 * 24 * 7, signed: false }, diff --git a/server/controller/article.js b/server/controller/article.js index 24e308f..83d6d41 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -22,6 +22,7 @@ exports.list = async (ctx, next) => { sort: { createdAt: -1 }, page, limit: pageSize, + select: '-content -renderedContent', populate: [ { path: 'tag', @@ -69,7 +70,7 @@ exports.list = async (ctx, next) => { // 文章列表不需要content和state options.select = '-content -renderedContent -state' } - + const articles = await ArticleModel.paginate(query, options).catch(err => { ctx.log.error(err.message) return null @@ -115,7 +116,6 @@ exports.item = async (ctx, next) => { } else { ctx.fail(-1, 'the article not found') } - } exports.create = async (ctx, next) => { @@ -161,7 +161,7 @@ exports.update = async (ctx, next) => { const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() const thumb = ctx.validateBody('thumb').optional().isString('the "thumb" parameter should be String type').val() - const issueNumber = ctx.validateBody('state').optional().toInt().gte(1, 'the "state" parameter must be 1 or older').val() + const issueNumber = ctx.validateBody('issue_number').optional().toInt().gte(1, 'the "issue_number" parameter must be 1 or older').val() const article = {} title && (article.title = title) @@ -181,11 +181,15 @@ exports.update = async (ctx, next) => { } const data = await ArticleModel.findByIdAndUpdate(id, article, { - new: true - }).catch(err => { - ctx.log.error(err.message) - return null - }) + new: true + }) + .select('-content -renderedContent') + .populate('tag') + .exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) if (data) { ctx.success(data) diff --git a/server/controller/auth.js b/server/controller/auth.js index 114e7e7..fabf344 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -28,7 +28,7 @@ exports.localLogin = async (ctx, next) => { .notEmpty() .isString('the "password" parameter should be String type') .val() - + const user = await UserModel.findOne({ name }).catch(err => { ctx.log.error(err.message) return null @@ -43,7 +43,7 @@ exports.localLogin = async (ctx, next) => { name: user.name }) ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: true }) - ctx.cookies.set('user_id', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) + ctx.cookies.set('jooger.me.userid', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) debug.success('login success, user: ', user._id) ctx.success({ id: user._id, @@ -64,11 +64,34 @@ exports.logout = async (ctx, next) => { name: ctx._user.name }, false) ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: 0, httpOnly: true }) - ctx.cookies.set('user_id', ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) + ctx.cookies.set('jooger.me.userid', ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) debug.success('logout success, user: ', ctx._user._id) ctx.success(null, 'logout success') } +exports.info = async (ctx, next) => { + const adminId = ctx._user._id + if (!adminId) { + return ctx.fail(401) + } + + const data = await UserModel.findById(adminId) + .select('-password') + .exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + if (data) { + ctx.success({ + info: data, + token: ctx.session._token + }) + } else { + ctx.fail(401) + } +} + // github login exports.githubLogin = async (ctx, next) => { await passport.authenticate('github', { @@ -84,7 +107,7 @@ exports.githubLogin = async (ctx, next) => { name: user.name }) ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: true }) - ctx.cookies.set('user_id', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) + ctx.cookies.set('jooger.me.userid', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) debug('github auth callback finish') debug.success('github login success, user: ', user._id) diff --git a/server/controller/music.js b/server/controller/music.js index c74413f..8fc8d8c 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -7,7 +7,7 @@ 'use strict' const NeteseMusic = require('simple-netease-cloud-music') -// const { fetchNE } = require('../service') +const { fetchNE } = require('../service') const { OptionModel } = require('../model') const debug = require('../util').setDebug('music') @@ -90,48 +90,27 @@ exports.cover = async (ctx, next) => { ctx.success(data) } -// TEST -// exports.test = async (ctx, next) => { -// const playListId = ctx.validateQuery('play_list_id') -// .required('the "play_list_id" parameter is required') -// .notEmpty() -// .isString('the "play_list_id" parameter should be String type') -// .val() -// const tracks = await fetchNE('playlist', playListId) -// .then(({ playlist }) => { -// return playlist.tracks.map(({ name, id, ar, al, dt, tns }) => ({ -// id, -// name, -// duration: dt, -// artists: ar.map(({ id, name }) => ({ id, name })), -// album: { -// name: al.name, -// cover: al.picUrl, -// tns: al.tns -// }, -// tns: tns || [] -// })) -// }) -// ctx.success(tracks) -// } - async function fetchSonglist (playListId) { - return neteaseMusic.playlist(playListId).then(({ playlist }) => { + return fetchNE('playlist', playListId).then(({ playlist }) => { return Promise.all( - playlist.tracks.map(track => { + playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { return Promise.all([ - neteaseMusic.url(track.id), - neteaseMusic.lyric(track.id) + neteaseMusic.url(id), + neteaseMusic.lyric(id) ]) .then(([song, lyric]) => [song.data[0] || null, lyric.nolyric ? '' : lyric.lrc.lyric]) .then(([song, lyric]) => { - const { id, name, dt, al, ar } = track return { id, name, duration: dt || 0, - album: al || {}, - artists: ar || [], + album: al && { + name: al.name, + cover: al.picUrl, + tns: al.tns + } || {}, + artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], + tns: tns || [], src: song.url, lyric } diff --git a/server/controller/user.js b/server/controller/user.js index 42580a6..d6c8865 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -47,6 +47,7 @@ exports.item = async (ctx, next) => { ctx.log.error(err.message) return null }) + if (data) { ctx.success(data) } else { diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index fdf34ea..71f4e86 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -31,12 +31,19 @@ function verifyToken () { } ctx.session._verify = false const token = ctx.cookies.get(config.auth.session.key) - + if (token) { - const decodedToken = await jwt.verify(token, config.auth.secrets) - if (decodedToken.exp > Math.floor(Date.now() / 1000)) { + let decodedToken = null + try { + decodedToken = await jwt.verify(token, config.auth.secrets) + } catch (err) { + ctx.fail(401) + } + + if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { // 已验证权限 ctx.session._verify = true + ctx.session._token = token } } await next() @@ -52,7 +59,7 @@ exports.isAuthenticated = () => { return ctx.fail(401) } - const userId = ctx.cookies.get('user_id', { signed: false }) + const userId = ctx.cookies.get('jooger.me.userid', { signed: false }) const user = await UserModel.findById(userId).exec().catch(err => { ctx.log.error(err.message) diff --git a/server/routes/backend.js b/server/routes/backend.js index e95bc3e..4283e8a 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -39,5 +39,6 @@ router.delete('/users/:id', isAuthenticated, user.delete) // Auth router.get('/auth/local/logout', isAuthenticated, auth.logout) router.post('/auth/local/login', auth.localLogin) +router.get('/auth/info', isAuthenticated, auth.info) module.exports = router From 41128c4eb0683637d16fcd71bb7e479370b3a41d Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 11 Oct 2017 00:03:21 +0800 Subject: [PATCH 028/208] [add] add formidable middleware for upload --- package-lock.json | 5 +++++ package.json | 1 + server/app.js | 1 + server/controller/article.js | 30 ++++++++++++------------- server/controller/tag.js | 25 +++++---------------- server/controller/upload.js | 13 +++++++++++ server/middleware/formidable.js | 39 +++++++++++++++++++++++++++++++++ server/middleware/index.js | 1 + server/model/schema/option.js | 1 - server/model/schema/tag.js | 3 +-- 10 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 server/controller/upload.js create mode 100644 server/middleware/formidable.js diff --git a/package-lock.json b/package-lock.json index fb751a1..f844a40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -633,6 +633,11 @@ "for-in": "1.0.2" } }, + "formidable": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.1.1.tgz", + "integrity": "sha1-lriIb3w8NQi5Mta9cMTTqI818ak=" + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", diff --git a/package.json b/package.json index b014732..8709434 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "big-integer": "^1.6.25", "crypto": "^1.0.1", "debug": "^2.6.9", + "formidable": "^1.1.1", "highlight.js": "^9.12.0", "koa": "^2.2.0", "koa-bodyparser": "^3.2.0", diff --git a/server/app.js b/server/app.js index 6540dee..43a9148 100644 --- a/server/app.js +++ b/server/app.js @@ -42,6 +42,7 @@ app.use(koaBunyanLogger()) app.use(bouncer.middleware()) app.use(middlewares.response) app.use(middlewares.error) +app.use(middlewares.formidable()) app.use(session(config.auth.session, app)) app.use(passport.initialize()) app.use(compress()) diff --git a/server/controller/article.js b/server/controller/article.js index 83d6d41..7174468 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -13,10 +13,10 @@ const { marked, isObjectId, createObjectId } = require('../util') exports.list = async (ctx, next) => { const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the "per_page" parameter should be greater than 0').val() const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'the "page" parameter should be greater than 0').val() - const state = ctx.validateQuery('state').defaultTo(1).toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() + const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() const tag = ctx.validateQuery('tag').optional().toString().val() const keyword = ctx.validateQuery('keyword').optional().toString().val() - + // 过滤条件 const options = { sort: { createdAt: -1 }, @@ -26,23 +26,24 @@ exports.list = async (ctx, next) => { populate: [ { path: 'tag', - select: 'name description', - match: { - forbidden: 0 - } + select: 'name description' } ] } // 查询条件 - const query = { state } + const query = {} + + if (state !== undefined) { + query.state = state + } // 搜索关键词 if (keyword) { const keywordReg = new RegExp(keyword) query.$or = [ - { title: keywordReg }, - { description: keywordReg } + { title: keywordReg } + // { description: keywordReg } ] } @@ -53,13 +54,12 @@ exports.list = async (ctx, next) => { query.tag = tag } else { // 普通字符串,需要先查到id - await TagModel.findOne({ name: tag }).exec() - .then(t => { - query.tag = t && t._id || createObjectId() - }) - .catch(() => { - query.tag = createObjectId() + const t = await TagModel.findOne({ name: tag }).exec() + .catch(err => { + ctx.log.error(err.message) + return null }) + query.tag = t ? t._id : createObjectId() } } diff --git a/server/controller/tag.js b/server/controller/tag.js index 403d7a0..5314c7f 100644 --- a/server/controller/tag.js +++ b/server/controller/tag.js @@ -13,7 +13,7 @@ exports.list = async (ctx, next) => { ctx.log.error(err.message) return null }) - + if (data) { for (let i = 0; i < data.length; i++) { if (data[i].toObject) { @@ -33,8 +33,9 @@ exports.list = async (ctx, next) => { exports.item = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - - const data = await TagModel.findById(id).exec().catch(err => { + + let data = await TagModel.findById(id).exec().catch(err => { + ctx.log.error(err.message) return null }) @@ -65,11 +66,6 @@ exports.create = async (ctx, next) => { .optional() .isString('the "description" parameter should be String type') .val() - const forbidden = ctx.validateBody('forbidden') - .defaultTo(0) - .toInt() - .isIn([0, 1], 'the "forbidden" parameter is not the expected value') - .val() const { length } = await TagModel.find({ name }).exec().catch(err => { ctx.log.error(err.message) @@ -79,8 +75,7 @@ exports.create = async (ctx, next) => { if (!length) { const data = await new TagModel({ name, - description, - forbidden + description }).save().catch(err => { ctx.log.error(err.message) return null @@ -106,19 +101,11 @@ exports.update = async (ctx, next) => { .optional() .isString('the "description" parameter should be String type') .val() - const forbidden = ctx.validateBody('forbidden') - .optional() - .toInt() - .isIn([0, 1], 'the "forbidden" parameter is not the expected value') - .val() const tag = {} - + name && (tag.name = name) description && (tag.description = description) - if (forbidden !== undefined) { - tag.forbidden = forbidden - } const data = await TagModel.findByIdAndUpdate(id, tag, { new: true diff --git a/server/controller/upload.js b/server/controller/upload.js new file mode 100644 index 0000000..fe2d547 --- /dev/null +++ b/server/controller/upload.js @@ -0,0 +1,13 @@ +/** + * @desc Upload controller + * @author Jooger + * @date 11 Oct 2017 + */ + +'use strict' + +const { formidable } = require('../middleware') + +exports.upload = async (ctx, next) => { + +} diff --git a/server/middleware/formidable.js b/server/middleware/formidable.js new file mode 100644 index 0000000..93c6522 --- /dev/null +++ b/server/middleware/formidable.js @@ -0,0 +1,39 @@ +/** + * @desc Formidable + * @author Jooger + * @date 10 Oct 2017 + */ + +'use strict' + +const formidable = require('formidable') + +const middleware = (opts = {}) => async (ctx, next) => { + const res = await middleware.parse(opts, ctx).catch(err => { + ctx.log.error(err.message) + return null + }) + if (res) { + ctx.request.body = res.fields + ctx.request.files = res.files + } + await next() +} + +middleware.parse = (opts = {}, ctx) => { + return new Promise((resolve, reject) => { + const form = new formidable.IncomingForm(opts) + for(const key in opts){ + form[key] = opts[key] + } + form.parse(ctx.req, (err, fields, files) => { + if (err) return reject(err) + resolve({ + fields, + files + }) + }) + }) +} + +module.exports = middleware diff --git a/server/middleware/index.js b/server/middleware/index.js index 56e4875..d1ef6b3 100644 --- a/server/middleware/index.js +++ b/server/middleware/index.js @@ -10,3 +10,4 @@ exports.error = require('./error') exports.response = require('./response') exports.authenticate = require('./authenticate') exports.header = require('./header') +exports.formidable = require('./formidable') diff --git a/server/model/schema/option.js b/server/model/schema/option.js index 6e2a2b5..c65929f 100644 --- a/server/model/schema/option.js +++ b/server/model/schema/option.js @@ -9,7 +9,6 @@ const mongoose = require('mongoose') const optionSchema = new mongoose.Schema({ - url: { type: String, default: '' }, title: { type: String, default: '' }, subtitle: { type: String, default: '' }, welcome: { type: String, default: '' }, diff --git a/server/model/schema/tag.js b/server/model/schema/tag.js index e13a75d..d6ba9c0 100644 --- a/server/model/schema/tag.js +++ b/server/model/schema/tag.js @@ -12,8 +12,7 @@ const tagSchema = new mongoose.Schema({ name: { type: String, required: true }, description: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, - forbidden: { type: Number, default: 0 } + updatedAt: { type: Date, default: Date.now } }) module.exports = tagSchema From 7de83d7766211c6f1f88c8ff95447d9b948fa9d7 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 11 Oct 2017 22:46:54 +0800 Subject: [PATCH 029/208] [optimize] optimize apis --- package-lock.json | 251 +++++++++++++++++++++++++++++- package.json | 1 + server/app.js | 3 +- server/controller/article.js | 2 +- server/controller/music.js | 50 +++--- server/controller/option.js | 40 +++-- server/controller/upload.js | 13 -- server/middleware/formidable.js | 4 +- server/middleware/header.js | 8 +- server/routes/backend.js | 5 +- server/service/github-userinfo.js | 31 ++-- 11 files changed, 333 insertions(+), 75 deletions(-) delete mode 100644 server/controller/upload.js diff --git a/package-lock.json b/package-lock.json index f844a40..d836541 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,18 @@ "negotiator": "0.6.1" } }, + "aliyun-sdk": { + "version": "1.10.18", + "resolved": "https://registry.npmjs.org/aliyun-sdk/-/aliyun-sdk-1.10.18.tgz", + "integrity": "sha1-3OLLNdO+r1Js5CxibGJAPsl0QWg=", + "requires": { + "node_memcached": "1.1.3", + "pomelo-protobuf": "0.4.0", + "protobufjs": "4.1.3", + "xml2js": "0.4.4", + "xmlbuilder": "2.6.5" + } + }, "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -79,6 +91,15 @@ "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", "dev": true }, + "ascli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", + "integrity": "sha1-vPpZdKYvGOgcq660lzKrSoj5Brw=", + "requires": { + "colour": "0.7.1", + "optjs": "3.2.2" + } + }, "async": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", @@ -225,6 +246,11 @@ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" }, + "bufferview": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bufferview/-/bufferview-1.0.1.tgz", + "integrity": "sha1-ev10pF+Tf6QiodM4wIu/3HbNcl0=" + }, "bunyan": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", @@ -235,6 +261,15 @@ "safe-json-stringify": "1.0.4" } }, + "bytebuffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-4.1.0.tgz", + "integrity": "sha1-TFgmngUqseSx9/82T9+zzogpBqo=", + "requires": { + "bufferview": "1.0.1", + "long": "2.4.0" + } + }, "bytes": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", @@ -291,6 +326,36 @@ "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", "dev": true }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -310,8 +375,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "color-convert": { "version": "1.9.0", @@ -328,6 +392,11 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colour": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", + "integrity": "sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g=" + }, "compressible": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.11.tgz", @@ -1697,7 +1766,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "optional": true, "requires": { "once": "1.4.0", "wrappy": "1.0.2" @@ -1714,6 +1782,11 @@ "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", "dev": true }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -2160,6 +2233,14 @@ "package-json": "4.0.1" } }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "1.0.0" + } + }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -2265,6 +2346,11 @@ "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", "dev": true }, + "long": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", + "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=" + }, "lowercase-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", @@ -2538,6 +2624,14 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "node_memcached": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/node_memcached/-/node_memcached-1.1.3.tgz", + "integrity": "sha1-icFSr4itKIF/ANiRyZBFHV1xLqg=", + "requires": { + "debug": "2.6.9" + } + }, "nodemon": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.12.1.tgz", @@ -2586,8 +2680,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth": { "version": "0.9.15", @@ -2634,6 +2727,19 @@ "wordwrap": "0.0.3" } }, + "optjs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", + "integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "1.0.0" + } + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -2753,6 +2859,11 @@ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, + "pomelo-protobuf": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/pomelo-protobuf/-/pomelo-protobuf-0.4.0.tgz", + "integrity": "sha1-5F6aCkRusYZn4MbhPutT1Hrdvag=" + }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -2770,6 +2881,73 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, + "protobufjs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-4.1.3.tgz", + "integrity": "sha1-jjbRsCJsu2jWR+S0TCoUTzfyd54=", + "requires": { + "ascli": "1.0.1", + "bytebuffer": "4.1.0", + "glob": "5.0.15", + "yargs": "3.32.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "requires": { + "camelcase": "2.1.1", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "os-locale": "1.4.0", + "string-width": "1.0.2", + "window-size": "0.1.4", + "y18n": "3.2.1" + } + } + } + }, "ps-tree": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", @@ -2977,6 +3155,11 @@ "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", "optional": true }, + "sax": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", + "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=" + }, "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", @@ -3354,6 +3537,35 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3376,11 +3588,40 @@ "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", "dev": true }, + "xml2js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=", + "requires": { + "sax": "0.6.1", + "xmlbuilder": "2.6.5" + } + }, + "xmlbuilder": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.6.5.tgz", + "integrity": "sha1-b/etYPty0idk8AehZLd/K/FABSY=", + "requires": { + "lodash": "3.10.1" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", diff --git a/package.json b/package.json index 8709434..8a874a0 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ }, "bin": "./node_modules/.bin/", "dependencies": { + "aliyun-sdk": "^1.10.18", "axios": "^0.16.2", "bcryptjs": "^2.4.3", "big-integer": "^1.6.25", diff --git a/server/app.js b/server/app.js index 43a9148..74bad5b 100644 --- a/server/app.js +++ b/server/app.js @@ -42,7 +42,8 @@ app.use(koaBunyanLogger()) app.use(bouncer.middleware()) app.use(middlewares.response) app.use(middlewares.error) -app.use(middlewares.formidable()) +// form parse +// app.use(middlewares.formidable()) app.use(session(config.auth.session, app)) app.use(passport.initialize()) app.use(compress()) diff --git a/server/controller/article.js b/server/controller/article.js index 7174468..aa42dc1 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -16,7 +16,7 @@ exports.list = async (ctx, next) => { const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() const tag = ctx.validateQuery('tag').optional().toString().val() const keyword = ctx.validateQuery('keyword').optional().toString().val() - + // 过滤条件 const options = { sort: { createdAt: -1 }, diff --git a/server/controller/music.js b/server/controller/music.js index 8fc8d8c..95bcff5 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -16,30 +16,35 @@ const neteaseMusic = new NeteseMusic() let songListMap = {} exports.list = async (ctx, next) => { - // const playListId = ctx.validateQuery('play_list_id') - // .required('the "play_list_id" parameter is required') - // .notEmpty() - // .isString('the "play_list_id" parameter should be String type') - // .val() + if (ctx._isAuthenticated) { + const playListId = ctx.validateQuery('play_list_id') + .required('the "play_list_id" parameter is required') + .notEmpty() + .isString('the "play_list_id" parameter should be String type') + .val() + + const data = await fetchSonglist(playListId) + ctx.success(data) + } else { + const option = await OptionModel.findOne({}).exec().catch(err => { + ctx.log.error(err.message) + return null + }) - const option = await OptionModel.findOne({}).exec().catch(err => { - ctx.log.error(err.message) - return null - }) + if (!option || !option.musicId) { + return ctx.fail() + } - if (!option || !option.musicId) { - return ctx.fail() - } + const musicId = option.musicId - const musicId = option.musicId + if (songListMap[musicId] && songListMap[musicId].length) { + return ctx.success(songListMap[musicId]) + } - if (songListMap[musicId]) { - return ctx.success(songListMap[musicId]) + const data = await fetchSonglist(musicId) + songListMap[musicId] = data + ctx.success(data) } - - const data = await fetchSonglist(musicId) - songListMap[musicId] = data - ctx.success(data) } exports.item = async (ctx, next) => { @@ -93,7 +98,7 @@ exports.cover = async (ctx, next) => { async function fetchSonglist (playListId) { return fetchNE('playlist', playListId).then(({ playlist }) => { return Promise.all( - playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { + !playlist ? [] : playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { return Promise.all([ neteaseMusic.url(id), neteaseMusic.lyric(id) @@ -117,6 +122,9 @@ async function fetchSonglist (playListId) { }) } )) + }).catch(err => { + debug.error(err.message) + return [] }) } @@ -139,7 +147,7 @@ exports.updateSongListMap = async function () { const ids = Object.keys(songListMap) const list = await Promise.all(ids.map(playListId => fetchSonglist(playListId))) .catch(err => debug.error('timed update music failed, err: ', err.message)) - + if (list && list.length === ids.length) { ids.map((id, index) => { songListMap[id] = list[index] diff --git a/server/controller/option.js b/server/controller/option.js index fca9c03..76d3a6a 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -8,6 +8,7 @@ const { OptionModel } = require('../model') const { getGithubUsersInfo } = require('../service') +const { updateSongListMap } = require('./music') const debug = require('../util').setDebug('option') exports.data = async (ctx, next) => { @@ -26,7 +27,7 @@ exports.data = async (ctx, next) => { exports.update = async (ctx, next) => { const option = ctx.request.body - const data = await updateOption(option) + const data = await exports.updateOption(option) if (data) { ctx.success(data) @@ -45,12 +46,31 @@ exports.updateOption = async function (option = null) { } // 更新友链 - if (option.links) { - const githubNames = option.links.map(link => link.github) + option.links = await generateLinks(option.links) + + const data = await OptionModel.findOneAndUpdate({}, option, { new: true }).exec().catch(err => { + ctx.log.error(err.message) + return null + }) + + // 更新 music list + await updateSongListMap() + + if (data) { + debug.success('timed update option success...') + } + + return data +} + +// 更新友链 +async function generateLinks (links = []) { + if (links && links.length) { + const githubNames = links.map(link => link.github) const usersInfo = await getGithubUsersInfo(githubNames) if (usersInfo) { - option.links = option.links.map((link, index) => { + return links.map((link, index) => { const userInfo = usersInfo[index] if (userInfo) { link.avatar = userInfo.avatar_url @@ -61,17 +81,7 @@ exports.updateOption = async function (option = null) { }) } } - - const data = await OptionModel.findOneAndUpdate({}, option, { new: true }).exec().catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - debug.success('timed update option success...') - } - - return data + return links } // 每1小时更新一次 diff --git a/server/controller/upload.js b/server/controller/upload.js deleted file mode 100644 index fe2d547..0000000 --- a/server/controller/upload.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @desc Upload controller - * @author Jooger - * @date 11 Oct 2017 - */ - -'use strict' - -const { formidable } = require('../middleware') - -exports.upload = async (ctx, next) => { - -} diff --git a/server/middleware/formidable.js b/server/middleware/formidable.js index 93c6522..b9a6c07 100644 --- a/server/middleware/formidable.js +++ b/server/middleware/formidable.js @@ -22,11 +22,11 @@ const middleware = (opts = {}) => async (ctx, next) => { middleware.parse = (opts = {}, ctx) => { return new Promise((resolve, reject) => { - const form = new formidable.IncomingForm(opts) + const form = new formidable.IncomingForm() for(const key in opts){ form[key] = opts[key] } - form.parse(ctx.req, (err, fields, files) => { + form.parse(ctx.request, (err, fields, files) => { if (err) return reject(err) resolve({ fields, diff --git a/server/middleware/header.js b/server/middleware/header.js index d4eb2d0..3ef3e56 100644 --- a/server/middleware/header.js +++ b/server/middleware/header.js @@ -12,10 +12,10 @@ module.exports = async (ctx, next) => { const { request, response } = ctx const allowedOrigins = config.auth.allowedOrigins const origin = request.get('origin') || '' - const allowed = request.query._DEV_ || - origin.includes('localhost') || - origin.includes('127.0.0.1') || - allowedOrigins.find(item => origin.includes(item)) + const allowed = request.query._DEV_ + || origin.includes('localhost') + || origin.includes('127.0.0.1') + || allowedOrigins.find(item => origin.includes(item)) if (allowed) { response.set('Access-Control-Allow-Origin', origin) } diff --git a/server/routes/backend.js b/server/routes/backend.js index 4283e8a..ee4ce24 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -7,7 +7,7 @@ 'use strict' const router = require('koa-router')() -const { article, tag, option, user, auth } = require('../controller') +const { article, tag, option, user, auth, music } = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() @@ -36,6 +36,9 @@ router.get('/users/:id', isAuthenticated, user.item) router.patch('/users/:id', isAuthenticated, user.update) router.delete('/users/:id', isAuthenticated, user.delete) +// Music +router.get('/music/songs', isAuthenticated, music.list) + // Auth router.get('/auth/local/logout', isAuthenticated, auth.logout) router.post('/auth/local/login', auth.localLogin) diff --git a/server/service/github-userinfo.js b/server/service/github-userinfo.js index 64d45d5..9d15cc3 100644 --- a/server/service/github-userinfo.js +++ b/server/service/github-userinfo.js @@ -8,6 +8,8 @@ const axios = require('axios') const { setDebug } = require('../util') +const config = require('../config') +const { clientID, clientSecret } = config.sns.github const debug = setDebug('github:user') const getGithubUsersInfo = (githubNames = '') => { @@ -21,18 +23,23 @@ const getGithubUsersInfo = (githubNames = '') => { const task = githubNames.map(name => { debug('fetch github user [', name, ']') - return axios.get(`https://api.github.com/users/${name}`) - .then(res => { - if (res && res.status === 200) { - debug.success('fetch github user success [', name, ']') - return res.data - } - return null - }) - .catch(err => { - debug.error(err.message) - return null - }) + return axios.get(`https://api.github.com/users/${name}`, { + params: { + client_id: clientID, + client_secret: clientSecret + } + }).then(res => { + if (res && res.status === 200) { + debug.success('fetch github user success [', name, ']') + return res.data + } + return null + }) + .catch(err => { + console.error(err) + debug.error(err.message) + return null + }) }) return Promise.all(task) From e33945a1a38ddb220ba08cc53711ec3dcc7c30b0 Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 12 Oct 2017 18:53:33 +0800 Subject: [PATCH 030/208] [fix] fix tags delete api bug --- server/controller/article.js | 66 +++++++++++++++++++++++++++------ server/controller/index.js | 1 + server/controller/statistics.js | 7 ++++ server/controller/tag.js | 30 ++++++++++++++- server/model/schema/article.js | 4 +- server/routes/backend.js | 9 +++-- 6 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 server/controller/statistics.js diff --git a/server/controller/article.js b/server/controller/article.js index aa42dc1..2f7241d 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -98,7 +98,7 @@ exports.item = async (ctx, next) => { let queryPs = null // 只有前台博客访问文章的时候pv才+1 if (!ctx._isAuthenticated) { - queryPs = ArticleModel.findByIdAndUpdate(id, { $inc: { 'meta.pvs': 1 } }, { new: true }).select('-content') + queryPs = ArticleModel.findAndUpdate({ _id: id, state: 1 }, { $inc: { 'meta.pvs': 1 } }, { new: true }).select('-content') } else { queryPs = ArticleModel.findById(id) } @@ -130,22 +130,53 @@ exports.create = async (ctx, next) => { .isString('the "content" parameter should be String type') .val() const keywords = ctx.validateBody('keywords').optional().defaultTo([]).isArray('the "keywords" parameter should be Array type').val() + const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() const description = ctx.validateBody('description') .optional() .isString('the "description" parameter should be String type') .val() - const data = await new ArticleModel({ - title, - content, - renderedContent: marked(content), - keywords, - description - }).save().catch(err => { + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() + const thumb = ctx.validateBody('thumb').optional().isString('the "thumb" parameter should be String type').val() + const createdAt = ctx.validateBody('createdAt').optional().toString().val() + const permalink = ctx.validateBody('permalink') + .optional() + .isString('the "permalink" parameter should be String type') + .val() + + const article = {} + + title && (article.title = title) + keywords && (article.keywords = keywords) + description && (article.description = description) + tag && (article.tag = tag) + thumb && (article.thumb = thumb) + createdAt && (article.createdAt = new Date(createdAt)) + permalink && (article.permalink = permalink) + + if (state !== undefined) { + article.state = state + } + + if (content) { + article.content = content + article.renderedContent = marked(content) + } + + let data = await new ArticleModel(article).save().catch(err => { ctx.log.error(err.message) return null }) if (data) { + if (!data.permalink) { + // 更新永久链接 + data = await ArticleModel.findByIdAndUpdate(data._id, { + permalink: `https://jooger.me/blog/article/${data._id}` + }, { new : true }).exec().catch(err => { + ctx.log.error(err.message) + return data + }) + } ctx.success(data) } else { ctx.fail() @@ -161,15 +192,20 @@ exports.update = async (ctx, next) => { const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() const thumb = ctx.validateBody('thumb').optional().isString('the "thumb" parameter should be String type').val() - const issueNumber = ctx.validateBody('issue_number').optional().toInt().gte(1, 'the "issue_number" parameter must be 1 or older').val() + const createdAt = ctx.validateBody('createdAt').optional().toString().val() + const article = {} + const cache = await ArticleModel.findById(id).exec().catch(err => { + ctx.log.error(err.message) + return null + }) title && (article.title = title) keywords && (article.keywords = keywords) description && (article.description = description) tag && (article.tag = tag) thumb && (article.thumb = thumb) - issueNumber && (article.issueNumber = issueNumber) + createdAt && (article.createdAt = new Date(createdAt)) if (state !== undefined) { article.state = state @@ -180,10 +216,16 @@ exports.update = async (ctx, next) => { article.renderedContent = marked(content) } + if (cache) { + // 如果文章状态由草稿变为发布,更新发布时间 + if (cache.state !== article.state && article.state === 1) { + article.publishedAt = Date.now() + } + } + const data = await ArticleModel.findByIdAndUpdate(id, article, { new: true }) - .select('-content -renderedContent') .populate('tag') .exec() .catch(err => { @@ -246,7 +288,7 @@ async function getRelatedArticles (ctx, data) { ctx.log.error('related articles access failed, err: ', err.message) return null }) - + if (articles) { data.related = articles } diff --git a/server/controller/index.js b/server/controller/index.js index 0414f1d..e7cc184 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -12,3 +12,4 @@ exports.music = require('./music') exports.option = require('./option') exports.user = require('./user') exports.auth = require('./auth') +exports.statistics = require('./statistics') diff --git a/server/controller/statistics.js b/server/controller/statistics.js new file mode 100644 index 0000000..222187a --- /dev/null +++ b/server/controller/statistics.js @@ -0,0 +1,7 @@ +/** + * @desc Statistics controller + * @author Jooger + * @date 25 Sep 2017 + */ + +exports.data = async (ctx, next) => {} diff --git a/server/controller/tag.js b/server/controller/tag.js index 5314c7f..4c1bd9a 100644 --- a/server/controller/tag.js +++ b/server/controller/tag.js @@ -9,7 +9,18 @@ const { TagModel, ArticleModel } = require('../model') exports.list = async (ctx, next) => { - let data = await TagModel.find({}).sort('-createdAt').catch(err => { + const keyword = ctx.validateQuery('keyword').optional().toString().val() + + const query = {} + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { name: keywordReg } + ] + } + + const data = await TagModel.find(query).sort('-createdAt').catch(err => { ctx.log.error(err.message) return null }) @@ -129,6 +140,23 @@ exports.delete = async (ctx, next) => { }) if (data && data.result && data.result.ok) { + // 删除所有文章关联关系 + const articles = await ArticleModel.find({ tag: data._id }) + .exec() + .catch(err => { + ctx.log.error(err.message) + return [] + }) + // TODO: 这里应该需要一个容错机制,保证如果有一篇文章没有删除成功的话,需要在规定次数内反复删除 + await Promise.all( + articles.map(item => { + return ArticleModel.findByIdAndUpdate(item._id, { + tag: item.tag.filter(tag => tag.toString() !== data._id.toString()) + }).exec() + }) + ).catch(err => { + ctx.log.error(err.message) + }) ctx.success() } else { ctx.fail() diff --git a/server/model/schema/article.js b/server/model/schema/article.js index 15535f0..c4f5aca 100644 --- a/server/model/schema/article.js +++ b/server/model/schema/article.js @@ -26,8 +26,8 @@ const articleSchema = new mongoose.Schema({ thumb: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, // 文章状态 ( 0 草稿 | 1 已发布 ) state: { type: Number, default: 0 }, - // github issue - issueNumber: { type: Number, default: 1 }, + // 永久链接 + permalink: { type: String, validate: /\S+/ }, // 创建日期 createdAt: { type: Date, default: Date.now }, // 更新日期 diff --git a/server/routes/backend.js b/server/routes/backend.js index ee4ce24..0227761 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -7,14 +7,14 @@ 'use strict' const router = require('koa-router')() -const { article, tag, option, user, auth, music } = require('../controller') +const { article, tag, option, user, auth, music, statistics } = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() // Article router.get('/articles', isAuthenticated, article.list) router.get('/articles/:id', isAuthenticated, article.item) -router.post('/articles', isAuthenticated, article.create) +router.post('/articles', article.create) router.patch('/articles/:id', isAuthenticated, article.update) router.delete('/articles/:id', isAuthenticated, article.delete) router.post('/articles/:id/like', isAuthenticated, article.like) @@ -24,7 +24,7 @@ router.get('/tags', isAuthenticated, tag.list) router.get('/tags/:id', isAuthenticated, tag.item) router.post('/tags', isAuthenticated, tag.create) router.patch('/tags/:id', isAuthenticated, tag.update) -router.delete('/tags/:id', isAuthenticated, tag.delete) +router.delete('/tags/:id', tag.delete) // Option router.get('/options', isAuthenticated, option.data) @@ -44,4 +44,7 @@ router.get('/auth/local/logout', isAuthenticated, auth.logout) router.post('/auth/local/login', auth.localLogin) router.get('/auth/info', isAuthenticated, auth.info) +// Statistics +router.get('/statistics', isAuthenticated, statistics.data) + module.exports = router From 93369eb4ba7e8c27eddaaba029912d3481f8bea2 Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 12 Oct 2017 19:10:31 +0800 Subject: [PATCH 031/208] [update] update router --- server/config/index.js | 1 + server/routes/index.js | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index 0611f09..8ada77c 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -14,6 +14,7 @@ const baseConfig = { name: packageInfo.name, version: packageInfo.version, author: packageInfo.author || 'Jooger', + site: 'https://jooger.me', env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3001, diff --git a/server/routes/index.js b/server/routes/index.js index a082256..25cf782 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -6,23 +6,33 @@ 'use strict' -const router = require('koa-router')({ - prefix: '/api' -}) +const router = require('koa-router')() const frontend = require('./frontend') const backend = require('./backend') const { header } = require('../middleware') +const config = require('../config') module.exports = app => { router.use('*', header) + router.get('/', async (ctx, next) => { + ctx.body = { + name: config.name, + version: config.version, + author: config.author, + github: 'https://github.com/jo0ger', + site: config.site, + poweredBy: ['Koa2', 'MongoDB', 'Nginx'] + } + }) + router.use('/backend', backend.routes(), backend.allowedMethods()) router.use(frontend.routes(), frontend.allowedMethods()) - + router.all('*', (ctx,next)=> { ctx.fail(404, `${ctx.path} 不支持 ${ctx.method} 请求类型`) ctx.status = 404 }) - + app.use(router.routes(), router.allowedMethods()) } From 30b500590663894ce7d5aa0afbc41ae26f01920a Mon Sep 17 00:00:00 2001 From: Jooger Date: Fri, 13 Oct 2017 01:00:55 +0800 Subject: [PATCH 032/208] [update] update music crontab --- server/controller/music.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index 95bcff5..5955931 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -156,5 +156,5 @@ exports.updateSongListMap = async function () { } } -// 每1小时更新一次 -setInterval(exports.updateSongListMap, 1000 * 60 * 60) +// 每10分钟更新一次 +setInterval(exports.updateSongListMap, 1000 * 60 * 10) From 66153310a45594ea977698e99ffb2ba8048de3f5 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 17 Oct 2017 23:11:27 +0800 Subject: [PATCH 033/208] [fix] fix article controller bug --- server/controller/article.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controller/article.js b/server/controller/article.js index 2f7241d..3ea50e0 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -98,7 +98,7 @@ exports.item = async (ctx, next) => { let queryPs = null // 只有前台博客访问文章的时候pv才+1 if (!ctx._isAuthenticated) { - queryPs = ArticleModel.findAndUpdate({ _id: id, state: 1 }, { $inc: { 'meta.pvs': 1 } }, { new: true }).select('-content') + queryPs = ArticleModel.findOneAndUpdate({ _id: id, state: 1 }, { $inc: { 'meta.pvs': 1 } }, { new: true }).select('-content') } else { queryPs = ArticleModel.findById(id) } From eeaa8e9ef885df0b7f0a4175b6bb39b286036482 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 18 Oct 2017 19:30:23 +0800 Subject: [PATCH 034/208] [update] update music api , add nginx proxy wrapper --- server/controller/music.js | 5 +++-- server/controller/statistics.js | 1 + server/routes/frontend.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index 5955931..7abb212 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -7,13 +7,14 @@ 'use strict' const NeteseMusic = require('simple-netease-cloud-music') +const config = require('../config') const { fetchNE } = require('../service') const { OptionModel } = require('../model') const debug = require('../util').setDebug('music') const neteaseMusic = new NeteseMusic() -let songListMap = {} +const songListMap = {} exports.list = async (ctx, next) => { if (ctx._isAuthenticated) { @@ -116,7 +117,7 @@ async function fetchSonglist (playListId) { } || {}, artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], tns: tns || [], - src: song.url, + src: song && song.url ? `${config.site}/proxy?url=${song.url}` : '', lyric } }) diff --git a/server/controller/statistics.js b/server/controller/statistics.js index 222187a..79862bb 100644 --- a/server/controller/statistics.js +++ b/server/controller/statistics.js @@ -4,4 +4,5 @@ * @date 25 Sep 2017 */ +// TODO: 站内统计 exports.data = async (ctx, next) => {} diff --git a/server/routes/frontend.js b/server/routes/frontend.js index f089c60..86048b0 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -24,7 +24,7 @@ router.get('/tags/:id', tag.item) // Music router.get('/music/songs', music.list) -// router.get('/music/songs/:song_id', music.item) +router.get('/music/songs/:song_id', music.item) // router.get('/music/songs/:song_id/url', music.url) // router.get('/music/songs/:song_id/lyric', music.lyric) // router.get('/music/songs/cover/:cover_id', music.cover) From c856b4acce0dae39df5316d387bb8a694f455716 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 18 Oct 2017 19:30:23 +0800 Subject: [PATCH 035/208] [update] update music api , add nginx proxy wrapper --- server/controller/music.js | 6 ++++-- server/controller/statistics.js | 1 + server/routes/frontend.js | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index 5955931..f806bf9 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -7,13 +7,15 @@ 'use strict' const NeteseMusic = require('simple-netease-cloud-music') +const config = require('../config') const { fetchNE } = require('../service') const { OptionModel } = require('../model') const debug = require('../util').setDebug('music') +const isProd = process.env.NODE_ENV === 'production' const neteaseMusic = new NeteseMusic() -let songListMap = {} +const songListMap = {} exports.list = async (ctx, next) => { if (ctx._isAuthenticated) { @@ -116,7 +118,7 @@ async function fetchSonglist (playListId) { } || {}, artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], tns: tns || [], - src: song.url, + src: isProd ? (song && song.url ? `${config.site}/proxy?url=${song.url}` : '') : song.url, lyric } }) diff --git a/server/controller/statistics.js b/server/controller/statistics.js index 222187a..79862bb 100644 --- a/server/controller/statistics.js +++ b/server/controller/statistics.js @@ -4,4 +4,5 @@ * @date 25 Sep 2017 */ +// TODO: 站内统计 exports.data = async (ctx, next) => {} diff --git a/server/routes/frontend.js b/server/routes/frontend.js index f089c60..86048b0 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -24,7 +24,7 @@ router.get('/tags/:id', tag.item) // Music router.get('/music/songs', music.list) -// router.get('/music/songs/:song_id', music.item) +router.get('/music/songs/:song_id', music.item) // router.get('/music/songs/:song_id/url', music.url) // router.get('/music/songs/:song_id/lyric', music.lyric) // router.get('/music/songs/cover/:cover_id', music.cover) From 5e3035ff16e14852d77c71a117d639a512cd5772 Mon Sep 17 00:00:00 2001 From: Jooger Date: Fri, 20 Oct 2017 11:50:38 +0800 Subject: [PATCH 036/208] [update] update proxy --- server/controller/music.js | 10 ++++++---- server/mongo.js | 4 +--- server/util/index.js | 2 ++ server/util/proxy.js | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 server/util/proxy.js diff --git a/server/controller/music.js b/server/controller/music.js index f806bf9..5b98dd6 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -10,9 +10,10 @@ const NeteseMusic = require('simple-netease-cloud-music') const config = require('../config') const { fetchNE } = require('../service') const { OptionModel } = require('../model') -const debug = require('../util').setDebug('music') -const isProd = process.env.NODE_ENV === 'production' +const { proxy, setDebug } = require('../util') +const isProd = process.env.NODE_ENV === 'production' +const debug = setDebug('music') const neteaseMusic = new NeteseMusic() const songListMap = {} @@ -113,12 +114,13 @@ async function fetchSonglist (playListId) { duration: dt || 0, album: al && { name: al.name, - cover: al.picUrl, + cover: al.picUrl ? `${config.site}${proxy(al.picUrl)}` : '', tns: al.tns } || {}, artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], tns: tns || [], - src: isProd ? (song && song.url ? `${config.site}/proxy?url=${song.url}` : '') : song.url, + // src: isProd ? (song && song.url ? `${config.site}/proxy?url=${song.url}` : '') : song.url, + src: song && song.url ? `${config.site}${proxy(song.url)}` : '', lyric } }) diff --git a/server/mongo.js b/server/mongo.js index e06eb4a..4549887 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -10,7 +10,6 @@ const mongoose = require('mongoose') const config = require('./config') const { UserModel, OptionModel } = require('./model') const { updateOption } = require('./controller/option') -const { updateSongListMap } = require('./controller/music') const { bhash, setDebug } = require('./util') const debug = setDebug('mongo:connect') @@ -35,7 +34,6 @@ async function seedOption () { } updateOption() - updateSongListMap() } function seedAdmin () { @@ -44,7 +42,7 @@ function seedAdmin () { createAdmin() } }) - + function createAdmin () { new UserModel({ role: 0, diff --git a/server/util/index.js b/server/util/index.js index 4d35176..1b038b0 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -17,6 +17,8 @@ exports.marked = require('./marked') exports.encrypt = require('./encrypt') +exports.proxy = require('./proxy') + exports.createObjectId = () => mongoose.Types.ObjectId() exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) diff --git a/server/util/proxy.js b/server/util/proxy.js new file mode 100644 index 0000000..94e614c --- /dev/null +++ b/server/util/proxy.js @@ -0,0 +1,14 @@ +/** + * @desc Http url proxy replace + * @author Jooger + * @date 20 Oct 2017 + */ + +const prefix = 'http://' + +module.exports = (url = '') => { + if (url.startsWith(prefix)) { + return url.replace(prefix, '/proxy/') + } + return url +} From 6de17d4b0d2ddce6a6e76d23b4578e8c92416e5b Mon Sep 17 00:00:00 2001 From: Jooger Date: Fri, 20 Oct 2017 18:31:36 +0800 Subject: [PATCH 037/208] [update] update github auth login --- server/app.js | 1 + server/config/development.js | 7 +++++++ server/config/index.js | 6 +++--- server/config/production.js | 7 +++++++ server/controller/auth.js | 11 +++++------ server/controller/music.js | 5 ++--- server/middleware/authenticate.js | 9 +++++---- server/middleware/error.js | 2 +- server/routes/backend.js | 2 +- server/routes/frontend.js | 2 +- server/service/github-passport.js | 5 +++-- server/util/marked.js | 2 +- 12 files changed, 37 insertions(+), 22 deletions(-) diff --git a/server/app.js b/server/app.js index 74bad5b..1604042 100644 --- a/server/app.js +++ b/server/app.js @@ -46,6 +46,7 @@ app.use(middlewares.error) // app.use(middlewares.formidable()) app.use(session(config.auth.session, app)) app.use(passport.initialize()) +// app.use(passport.session()) app.use(compress()) // routes diff --git a/server/config/development.js b/server/config/development.js index dfe968c..b0fe2a7 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -11,5 +11,12 @@ const packageInfo = require('../../package.json') module.exports = { mongo: { uri: 'mongodb://127.0.0.1/jooger-me-dev' + }, + sns: { + github: { + clientID: '5b4d4a7945347d0fd2e2', + clientSecret: '8771bd9ae52749cc15b0c9e2c6cb4ecd7f39d9da', + callbackURL: 'http://127.0.0.1:3001/auth/github/login/callback' + } } } diff --git a/server/config/index.js b/server/config/index.js index 8ada77c..43d3adf 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -54,9 +54,9 @@ const baseConfig = { }, sns: { github: { - clientID: process.env.GITHUB_CLIENT_ID || '5b4d4a7945347d0fd2e2', - clientSecret: process.env.GITHUB_CLIENT_SECRET || '8771bd9ae52749cc15b0c9e2c6cb4ecd7f39d9da', - callbackURL: process.env.GITHUB_CALLBACK_URL || 'http://127.0.0.1:3001/api/auth/github/login/callback' + clientID: 'github client id', + clientSecret: 'github client secret', + callbackURL: 'github oauth callback url' } } } diff --git a/server/config/production.js b/server/config/production.js index 89763ab..ee25598 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -16,5 +16,12 @@ module.exports = { session: { domain: '.jooger.me' } + }, + sns: { + github: { + clientID: 'cc9133ad08a5fbc3b7bd', + clientSecret: '4b98cc1028eddc78e72d5e48657819be50581623', + callbackURL: 'https://api.jooger.me/auth/github/login/callback' + } } } diff --git a/server/controller/auth.js b/server/controller/auth.js index fabf344..264f24c 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -12,7 +12,6 @@ const config = require('../config') const { UserModel } = require('../model') const { bhash, bcompare, setDebug, signToken } = require('../util') const debug = setDebug('auth:github') - const { githubPassport } = require('../service') githubPassport.init(UserModel, config) @@ -42,7 +41,7 @@ exports.localLogin = async (ctx, next) => { id: user._id, name: user.name }) - ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: true }) + ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) ctx.cookies.set('jooger.me.userid', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) debug.success('login success, user: ', user._id) ctx.success({ @@ -63,7 +62,7 @@ exports.logout = async (ctx, next) => { id: ctx._user._id, name: ctx._user.name }, false) - ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: 0, httpOnly: true }) + ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) ctx.cookies.set('jooger.me.userid', ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) debug.success('logout success, user: ', ctx._user._id) ctx.success(null, 'logout success') @@ -98,7 +97,7 @@ exports.githubLogin = async (ctx, next) => { session: false }, (err, user) => { debug('github auth callback start') - const redirectUrl = ctx.session.passport.redirectUrl || '/' + const redirectUrl = ctx.session.passport.redirectUrl const cookieDomain = config.auth.session.domain || null const { session } = config.auth @@ -106,11 +105,11 @@ exports.githubLogin = async (ctx, next) => { id: user._id, name: user.name }) - ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: true }) + ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) ctx.cookies.set('jooger.me.userid', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) debug('github auth callback finish') - debug.success('github login success, user: ', user._id) + debug.success('github login success, userid: ', user._id, 'username: ', user.name) return ctx.redirect(redirectUrl) })(ctx) } diff --git a/server/controller/music.js b/server/controller/music.js index 5b98dd6..578dfb4 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -114,13 +114,12 @@ async function fetchSonglist (playListId) { duration: dt || 0, album: al && { name: al.name, - cover: al.picUrl ? `${config.site}${proxy(al.picUrl)}` : '', + cover: isProd ? (al.picUrl ? `${config.site}${proxy(al.picUrl)}` : '') : al.picUrl, tns: al.tns } || {}, artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], tns: tns || [], - // src: isProd ? (song && song.url ? `${config.site}/proxy?url=${song.url}` : '') : song.url, - src: song && song.url ? `${config.site}${proxy(song.url)}` : '', + src: isProd ? (song && song.url ? `${config.site}${proxy(song.url)}` : '') : song.url, lyric } }) diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index 71f4e86..7c5f3fe 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -12,6 +12,7 @@ const jwt = require('jsonwebtoken') const passport = require('koa-passport') const config = require('../config') const { UserModel } = require('../model') +const debug = require('../util').setDebug('auth') const isProd = process.env.NODE_ENV === 'production' // 开发环境下,请求携带_DEV_参数,视为已验证 @@ -80,12 +81,12 @@ exports.snsAuth = (name = '') => { verifyToken(), async (ctx, next) => { // 如果已经登录 + const redirectUrl = ctx.query.redirectUrl || config.site if (ctx.session._verify) { - return ctx.fail(-1, 'you have already logged in') - } - ctx.session.passport = { - redirectUrl: ctx.query.redirectUrl || '/' + debug.info('you have already logged in, redirecting') + return ctx.redirect(redirectUrl) } + ctx.session.passport = { redirectUrl } await next() }, passport.authenticate(name, { diff --git a/server/middleware/error.js b/server/middleware/error.js index 4d87123..8a2b55f 100644 --- a/server/middleware/error.js +++ b/server/middleware/error.js @@ -18,7 +18,7 @@ module.exports = async (ctx, next) => { ctx.fail(code, err.message) ctx.status = 200 - + if (code === 500) { ctx.log.error( { req: ctx.req, err }, diff --git a/server/routes/backend.js b/server/routes/backend.js index 0227761..7a3c591 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc backend api map * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 86048b0..aeb32bf 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc front api map * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/service/github-passport.js b/server/service/github-passport.js index ff0c0c3..420b0bc 100644 --- a/server/service/github-passport.js +++ b/server/service/github-passport.js @@ -29,7 +29,6 @@ exports.init = (UserModel, config) => { return null }) - if (user) { const userData = { name: profile.displayName || profile.username, @@ -39,6 +38,8 @@ exports.init = (UserModel, config) => { role: user.role } + userData.github.token = accessToken + const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData).exec().catch(err => { debug.error('user update error, err: ', err.message) }) || user @@ -68,7 +69,7 @@ exports.init = (UserModel, config) => { const data = await new UserModel(newUser).save().catch(err => { debug.error('user create fail, err: ', err.message) }) - + return end(null, data) } catch (err) { debug.error('github auth error') diff --git a/server/util/marked.js b/server/util/marked.js index 0cbad57..21f961d 100644 --- a/server/util/marked.js +++ b/server/util/marked.js @@ -49,7 +49,7 @@ renderer.image = function (href, title, text) { return ` ${text || title || href} `.replace(/\s+/g, ' ').replace('\n', '') From ab8dbbad45e7c5d6689f70866ea5648c5849c3de Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 21 Oct 2017 21:39:36 +0800 Subject: [PATCH 038/208] [fix] fix music lyric bug --- server/controller/music.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controller/music.js b/server/controller/music.js index 578dfb4..d0a6e2c 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -106,7 +106,7 @@ async function fetchSonglist (playListId) { neteaseMusic.url(id), neteaseMusic.lyric(id) ]) - .then(([song, lyric]) => [song.data[0] || null, lyric.nolyric ? '' : lyric.lrc.lyric]) + .then(([song, lyric]) => [song.data[0] || null, (lyric.nolyric || !lyric.lrc) ? '' : lyric.lrc.lyric]) .then(([song, lyric]) => { return { id, From 42a0ac25f49f6c2627a68d40555255d027121ac0 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 25 Oct 2017 11:28:14 +0800 Subject: [PATCH 039/208] [update] update controllers --- server/config/index.js | 2 +- server/controller/article.js | 8 ++++---- server/controller/music.js | 4 ++++ server/controller/option.js | 2 ++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index 43d3adf..6c5cc70 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -18,7 +18,7 @@ const baseConfig = { env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3001, - pageSize: 12, + pageSize: 15, codeMap: { '-1': 'fail', '200': 'success', diff --git a/server/controller/article.js b/server/controller/article.js index 3ea50e0..9d62759 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -282,7 +282,7 @@ async function getRelatedArticles (ctx, data) { data.related = [] if (data && data.tag && data.tag.length) { const articles = await ArticleModel.find({ _id: { $nin: [ data._id ] }, state: 1, tag: { $in: data.tag.map(t => t._id) }}) - .select('title thumb createdAt meta') + .select('title thumb createdAt publishedAt meta') .exec() .catch(err => { ctx.log.error('related articles access failed, err: ', err.message) @@ -290,7 +290,7 @@ async function getRelatedArticles (ctx, data) { }) if (articles) { - data.related = articles + data.related = articles.slice(0, 5) } } } @@ -308,7 +308,7 @@ async function getSiblingArticles (ctx, data) { query.state = 1 } let prev = await ArticleModel.findOne(query) - .select('title createdAt thumb') + .select('title createdAt publishedAt thumb') .sort('-createdAt') .lt('createdAt', data.createdAt) .exec() @@ -317,7 +317,7 @@ async function getSiblingArticles (ctx, data) { return null }) let next = await ArticleModel.findOne(query) - .select('title createdAt thumb') + .select('title createdAt publishedAt thumb') .sort('createdAt') .gt('createdAt', data.createdAt) .exec() diff --git a/server/controller/music.js b/server/controller/music.js index d0a6e2c..33eeb4e 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -157,6 +157,10 @@ exports.updateSongListMap = async function () { }) debug.success('timed update music success...') } + // GC + option = null + ids = null + list = null } // 每10分钟更新一次 diff --git a/server/controller/option.js b/server/controller/option.js index 76d3a6a..7c7f4ae 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -60,6 +60,8 @@ exports.updateOption = async function (option = null) { debug.success('timed update option success...') } + // GC + option = null return data } From 9f6ed30f2de041912fdfc52d43ee5c2242e4a3fb Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 25 Oct 2017 13:50:40 +0800 Subject: [PATCH 040/208] [fix] fix gc bug --- server/controller/music.js | 4 ---- server/controller/option.js | 3 --- 2 files changed, 7 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index 33eeb4e..d0a6e2c 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -157,10 +157,6 @@ exports.updateSongListMap = async function () { }) debug.success('timed update music success...') } - // GC - option = null - ids = null - list = null } // 每10分钟更新一次 diff --git a/server/controller/option.js b/server/controller/option.js index 7c7f4ae..c7bb460 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -59,9 +59,6 @@ exports.updateOption = async function (option = null) { if (data) { debug.success('timed update option success...') } - - // GC - option = null return data } From 306cd1501fab111eb4c90133b627fa6f302c6a54 Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 26 Oct 2017 12:33:38 +0800 Subject: [PATCH 041/208] [feature] add article category api --- README.md | 18 +++- server/controller/article.js | 75 +++++++++++---- server/controller/category.js | 164 ++++++++++++++++++++++++++++++++ server/controller/index.js | 1 + server/model/schema/article.js | 2 + server/model/schema/category.js | 18 ++++ server/model/schema/index.js | 2 +- server/model/schema/log.js | 2 +- server/model/schema/tag.js | 2 +- server/mongo.js | 2 + server/routes/backend.js | 10 +- server/routes/frontend.js | 6 +- 12 files changed, 278 insertions(+), 24 deletions(-) create mode 100644 server/controller/category.js create mode 100644 server/model/schema/category.js diff --git a/README.md b/README.md index 49268ff..c1cc564 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ -# jooger.me-server +## jooger.me-server My blog's api server build by koa2 and mongoose + +## TODOS + +* ~~音乐api~~ (2017.9.26) + +* ~~Github oauth 代理~~ (2017.9.28) + +* ~~文章分类api~~ (2017.10.26) + +* 评论api + +* 消息api + +* 日志api + +* 统计api diff --git a/server/controller/article.js b/server/controller/article.js index 9d62759..80e8b48 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -7,13 +7,14 @@ 'use strict' const config = require('../config') -const { ArticleModel, TagModel } = require('../model') +const { ArticleModel, CategoryModel, TagModel } = require('../model') const { marked, isObjectId, createObjectId } = require('../util') exports.list = async (ctx, next) => { const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the "per_page" parameter should be greater than 0').val() const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'the "page" parameter should be greater than 0').val() const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() + const category = ctx.validateQuery('category').optional().toString().val() const tag = ctx.validateQuery('tag').optional().toString().val() const keyword = ctx.validateQuery('keyword').optional().toString().val() @@ -24,6 +25,10 @@ exports.list = async (ctx, next) => { limit: pageSize, select: '-content -renderedContent', populate: [ + { + path: 'category', + select: 'name description' + }, { path: 'tag', select: 'name description' @@ -43,10 +48,25 @@ exports.list = async (ctx, next) => { const keywordReg = new RegExp(keyword) query.$or = [ { title: keywordReg } - // { description: keywordReg } ] } + // 分类 + if (category) { + // 如果是id + if (isObjectId(category)) { + query.category = category + } else { + // 普通字符串,需要先查到id + const c = await CategoryModel.findOne({ name: category }).exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + query.category = c ? c._id : createObjectId() + } + } + // 标签 if (tag) { // 如果是id @@ -103,7 +123,16 @@ exports.item = async (ctx, next) => { queryPs = ArticleModel.findById(id) } - data = await queryPs.populate('tag').exec().catch(err => { + data = await queryPs.populate([ + { + path: 'category', + select: 'name description' + }, + { + path: 'tag', + select: 'name description' + } + ]).exec().catch(err => { ctx.log.error(err.message) return null }) @@ -130,6 +159,7 @@ exports.create = async (ctx, next) => { .isString('the "content" parameter should be String type') .val() const keywords = ctx.validateBody('keywords').optional().defaultTo([]).isArray('the "keywords" parameter should be Array type').val() + const category = ctx.validateBody('category').optional().isObjectId().val() const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() const description = ctx.validateBody('description') .optional() @@ -148,6 +178,7 @@ exports.create = async (ctx, next) => { title && (article.title = title) keywords && (article.keywords = keywords) description && (article.description = description) + category && (article.category = category) tag && (article.tag = tag) thumb && (article.thumb = thumb) createdAt && (article.createdAt = new Date(createdAt)) @@ -171,8 +202,10 @@ exports.create = async (ctx, next) => { if (!data.permalink) { // 更新永久链接 data = await ArticleModel.findByIdAndUpdate(data._id, { - permalink: `https://jooger.me/blog/article/${data._id}` - }, { new : true }).exec().catch(err => { + permalink: `${config.site}/blog/article/${data._id}` + }, { + new : true + }).exec().catch(err => { ctx.log.error(err.message) return data }) @@ -189,6 +222,7 @@ exports.update = async (ctx, next) => { const content = ctx.validateBody('content').optional().isString('the "content" parameter should be String type').val() const keywords = ctx.validateBody('keywords').optional().isArray('the "keywords" parameter should be Array type').val() const description = ctx.validateBody('description').optional().isString('the "description" parameter should be String type').val() + const category = ctx.validateBody('category').optional().isObjectId().val() const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() const thumb = ctx.validateBody('thumb').optional().isString('the "thumb" parameter should be String type').val() @@ -203,6 +237,7 @@ exports.update = async (ctx, next) => { title && (article.title = title) keywords && (article.keywords = keywords) description && (article.description = description) + category && (article.category = category) tag && (article.tag = tag) thumb && (article.thumb = thumb) createdAt && (article.createdAt = new Date(createdAt)) @@ -226,7 +261,7 @@ exports.update = async (ctx, next) => { const data = await ArticleModel.findByIdAndUpdate(id, article, { new: true }) - .populate('tag') + .populate('category tag') .exec() .catch(err => { ctx.log.error(err.message) @@ -274,24 +309,28 @@ exports.like = async (ctx, next) => { } /** - * 获取相关文章 + * 根据标签获取相关文章 * @param {} ctx koa ctx * @param {} data 文章数据 */ async function getRelatedArticles (ctx, data) { data.related = [] - if (data && data.tag && data.tag.length) { - const articles = await ArticleModel.find({ _id: { $nin: [ data._id ] }, state: 1, tag: { $in: data.tag.map(t => t._id) }}) - .select('title thumb createdAt publishedAt meta') - .exec() - .catch(err => { - ctx.log.error('related articles access failed, err: ', err.message) - return null - }) + let { _id, tag = [] } = data + const articles = await ArticleModel.find({ + _id: { $nin: [ _id ] }, + state: 1, + tag: { $in: tag.map(t => t._id) }} + ) + .select('title thumb createdAt publishedAt meta') + .exec() + .catch(err => { + ctx.log.error('related articles access failed, err: ', err.message) + return null + }) - if (articles) { - data.related = articles.slice(0, 5) - } + if (articles) { + // 取前5篇 + data.related = articles.slice(0, 5) } } diff --git a/server/controller/category.js b/server/controller/category.js new file mode 100644 index 0000000..57b6ed0 --- /dev/null +++ b/server/controller/category.js @@ -0,0 +1,164 @@ +/** + * @desc Category controll + * @author Jooger + * @date 26 Oct 2017 + */ + +'use strict' + +const { CategoryModel, ArticleModel } = require('../model') + +exports.list = async (ctx, next) => { + const keyword = ctx.validateQuery('keyword').optional().toString().val() + + const query = {} + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { name: keywordReg } + ] + } + + const data = await CategoryModel.find(query).sort('-createdAt').catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + for (let i = 0; i < data.length; i++) { + if (data[i].toObject) { + data[i] = data[i].toObject() + } + const articles = await ArticleModel.find({ category: data[i]._id }).exec().catch(err => { + ctx.log.error(err.message) + return [] + }) + data[i].count = articles.length + } + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.item = async (ctx, next) => { + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + + let data = await CategoryModel.findById(id).exec().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + data = data.toObject() + const articles = await ArticleModel.find({ tag: id }) + .select('-tag') + .exec() + .catch(err => { + ctx.log.error(err.message) + return [] + }) + data.articles = articles + data.articles_count = articles.length + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.create = async (ctx, next) => { + const name = ctx.validateBody('name') + .required('the "name" parameter is required') + .notEmpty() + .isString('the "name" parameter should be String type') + .val() + const description = ctx.validateBody('description') + .optional() + .isString('the "description" parameter should be String type') + .val() + + const { length } = await CategoryModel.find({ name }).exec().catch(err => { + ctx.log.error(err.message) + return [] + }) + + if (!length) { + const data = await new CategoryModel({ + name, + description + }).save().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + return ctx.success(data) + } else { + ctx.fail() + } + } else { + ctx.fail(-1, `the tag(${name}) is already exist`) + } +} + +exports.update = async (ctx, next) => { + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const name = ctx.validateBody('name') + .optional() + .isString('the "name" parameter should be String type') + .val() + const description = ctx.validateBody('description') + .optional() + .isString('the "description" parameter should be String type') + .val() + + const tag = {} + + name && (tag.name = name) + description && (tag.description = description) + + const data = await CategoryModel.findByIdAndUpdate(id, tag, { + new: true + }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.delete = async (ctx, next) => { + const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const data = await CategoryModel.remove({ _id: id }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data && data.result && data.result.ok) { + // 删除所有文章关联关系 + const articles = await ArticleModel.find({ tag: data._id }) + .exec() + .catch(err => { + ctx.log.error(err.message) + return [] + }) + // TODO: 这里应该需要一个容错机制,保证如果有一篇文章没有删除成功的话,需要在规定次数内反复删除 + await Promise.all( + articles.map(item => { + return ArticleModel.findByIdAndUpdate(item._id, { + tag: item.tag.filter(tag => tag.toString() !== data._id.toString()) + }).exec() + }) + ).catch(err => { + ctx.log.error(err.message) + }) + ctx.success() + } else { + ctx.fail() + } +} diff --git a/server/controller/index.js b/server/controller/index.js index e7cc184..1d358fd 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -7,6 +7,7 @@ 'use strict' exports.article = require('./article') +exports.category = require('./category') exports.tag = require('./tag') exports.music = require('./music') exports.option = require('./option') diff --git a/server/model/schema/article.js b/server/model/schema/article.js index c4f5aca..f89ef60 100644 --- a/server/model/schema/article.js +++ b/server/model/schema/article.js @@ -20,6 +20,8 @@ const articleSchema = new mongoose.Schema({ content: { type: String, required: true, validate: /\S+/ }, // markdown渲染后的htmln内容 renderedContent: { type: String, required: true, validate: /\S+/ }, + // 分类 + category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' }, // 标签 tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) diff --git a/server/model/schema/category.js b/server/model/schema/category.js new file mode 100644 index 0000000..401d5dc --- /dev/null +++ b/server/model/schema/category.js @@ -0,0 +1,18 @@ +/** + * @desc Category + * @author Jooger + * @date 26 Oct 2017 + */ + +'use strict' + +const mongoose = require('mongoose') + +const categorySchema = new mongoose.Schema({ + name: { type: String, required: true }, + description: { type: String, default: '' }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now } +}) + +module.exports = categorySchema diff --git a/server/model/schema/index.js b/server/model/schema/index.js index 0406842..eea8ec1 100644 --- a/server/model/schema/index.js +++ b/server/model/schema/index.js @@ -8,7 +8,7 @@ exports.article = require('./article') exports.comment = require('./comment') +exports.category = require('./category') exports.tag = require('./tag') exports.user = require('./user') -// exports.log = require('./log') exports.option = require('./option') diff --git a/server/model/schema/log.js b/server/model/schema/log.js index ce73b00..a8934e2 100644 --- a/server/model/schema/log.js +++ b/server/model/schema/log.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc Site Log * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/model/schema/tag.js b/server/model/schema/tag.js index d6ba9c0..d742988 100644 --- a/server/model/schema/tag.js +++ b/server/model/schema/tag.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc Tag * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/mongo.js b/server/mongo.js index 4549887..313fbf3 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -26,6 +26,7 @@ module.exports = function () { seedAdmin() } +// 参数初始化 async function seedOption () { const option = await OptionModel.findOne().exec().catch(err => debug.error(err.message)) @@ -36,6 +37,7 @@ async function seedOption () { updateOption() } +// 管理员初始化 function seedAdmin () { UserModel.findOne({ role: 0 }).exec().then(data => { if (!data) { diff --git a/server/routes/backend.js b/server/routes/backend.js index 7a3c591..d029543 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -7,7 +7,7 @@ 'use strict' const router = require('koa-router')() -const { article, tag, option, user, auth, music, statistics } = require('../controller') +const { article, category, tag, option, user, auth, music, statistics } = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() @@ -26,6 +26,13 @@ router.post('/tags', isAuthenticated, tag.create) router.patch('/tags/:id', isAuthenticated, tag.update) router.delete('/tags/:id', tag.delete) +// Category +router.get('/categories', isAuthenticated, category.list) +router.get('/categories/:id', isAuthenticated, category.item) +router.post('/categories', isAuthenticated, category.create) +router.patch('/categories/:id', isAuthenticated, category.update) +router.delete('/categories/:id', category.delete) + // Option router.get('/options', isAuthenticated, option.data) router.patch('/options', isAuthenticated, option.update) @@ -45,6 +52,7 @@ router.post('/auth/local/login', auth.localLogin) router.get('/auth/info', isAuthenticated, auth.info) // Statistics +// TODO: router.get('/statistics', isAuthenticated, statistics.data) module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js index aeb32bf..6ac9cea 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -7,7 +7,7 @@ 'use strict' const router = require('koa-router')() -const { article, tag, music, option, user, auth } = require('../controller') +const { article, category, tag, music, option, user, auth } = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() const snsAuth = authenticate.snsAuth @@ -22,6 +22,10 @@ router.post('/articles/:id/like', article.like) router.get('/tags', tag.list) router.get('/tags/:id', tag.item) +// Category +router.get('/categories', category.list) +router.get('/categories/:id', category.item) + // Music router.get('/music/songs', music.list) router.get('/music/songs/:song_id', music.item) From b2c8c86d482a395112d7e784bcb012a601298455 Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 26 Oct 2017 15:33:30 +0800 Subject: [PATCH 042/208] [update] article list api support sort --- server/controller/article.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/server/controller/article.js b/server/controller/article.js index 80e8b48..f6f8726 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -17,6 +17,17 @@ exports.list = async (ctx, next) => { const category = ctx.validateQuery('category').optional().toString().val() const tag = ctx.validateQuery('tag').optional().toString().val() const keyword = ctx.validateQuery('keyword').optional().toString().val() + // 排序仅后台能用,且order和sortBy需同时传入才起作用 + // -1 desc | 1 asc + const order = ctx.validateQuery('order').optional().toInt().isIn( + [-1, 1], + 'invalid "order" parameter, optional value: -1 or 1' + ).val() + // createdAt | updatedAt | publishedAt | meta.ups | meta.pvs | meta.comments + const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn( + ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], + 'invalid "sort_by" parameter' + ).val() // 过滤条件 const options = { @@ -89,6 +100,12 @@ exports.list = async (ctx, next) => { query.state = 1 // 文章列表不需要content和state options.select = '-content -renderedContent -state' + } else { + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } } const articles = await ArticleModel.paginate(query, options).catch(err => { From bf024bad576e53fc4d89c3a5b6e511ec074b2860 Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 26 Oct 2017 19:20:40 +0800 Subject: [PATCH 043/208] [update] update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index c1cc564..678b2c8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![GitHub issues](https://img.shields.io/github/issues/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/issues) +[![GitHub forks](https://img.shields.io/github/forks/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/network) +[![GitHub stars](https://img.shields.io/github/stars/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/stargazers) + ## jooger.me-server My blog's api server build by koa2 and mongoose From 6c670d262a5534cd62c1c4a4ffead0575a36deec Mon Sep 17 00:00:00 2001 From: Jooger Date: Fri, 27 Oct 2017 19:02:05 +0800 Subject: [PATCH 044/208] [update] cheers v1.1, supports redis cache, and update crontab service --- README.md | 2 + bin/www | 8 +- package-lock.json | 81 +++++++++++-------- package.json | 6 +- server/app.js | 8 +- server/config/index.js | 6 +- server/controller/article.js | 3 +- server/controller/auth.js | 20 ++--- server/controller/music.js | 124 +++++++++++++++--------------- server/controller/option.js | 28 ++++--- server/middleware/authenticate.js | 33 +++----- server/middleware/error.js | 1 + server/middleware/formidable.js | 2 +- server/mongo.js | 33 +++++--- server/redis.js | 70 +++++++++++++++++ server/routes/backend.js | 3 + server/routes/frontend.js | 5 +- server/service/crontab.js | 18 +++++ server/service/github-passport.js | 18 ++--- server/service/github-userinfo.js | 10 +-- server/service/index.js | 3 +- server/service/netease-music.js | 4 +- server/util/debug.js | 2 +- server/util/index.js | 16 +++- server/util/proxy.js | 2 +- 25 files changed, 317 insertions(+), 189 deletions(-) create mode 100644 server/redis.js create mode 100644 server/service/crontab.js diff --git a/README.md b/README.md index 678b2c8..8fbafc7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ My blog's api server build by koa2 and mongoose * ~~文章分类api~~ (2017.10.26) +* ~~Redis缓存部分数据~~ (2017.10.27 v1.1) + * 评论api * 消息api diff --git a/bin/www b/bin/www index 2a9184b..fe53d79 100755 --- a/bin/www +++ b/bin/www @@ -6,7 +6,7 @@ const http = require('http') const app = require('../server/app') -const debug = require('../server/util').setDebug() +const debug = require('../server/util').getDebug('App') const config = require('../server/config') /** @@ -84,7 +84,7 @@ function onError(error) { function onListening() { const addr = server.address() const bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port - debug('Listening on ' + bind) + ? '管道:' + addr + : '端口:' + addr.port + debug.success('启动成功,', bind) } diff --git a/package-lock.json b/package-lock.json index d836541..00d42ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -155,9 +155,9 @@ "dev": true }, "bluebird": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz", - "integrity": "sha1-AkpVFylTCIV/FPkfEQb8O1VfRGs=" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" }, "boxen": { "version": "1.2.1", @@ -547,6 +547,11 @@ "is-obj": "1.0.1" } }, + "double-ended-queue": { + "version": "2.1.0-0", + "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" + }, "dtrace-provider": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", @@ -2467,12 +2472,12 @@ "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" }, "mongodb": { - "version": "2.2.31", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.31.tgz", - "integrity": "sha1-GUBEXGYeGSF7s7+CRdmFSq71SNs=", + "version": "2.2.33", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.33.tgz", + "integrity": "sha1-tTfEcdNKZlG0jzb9vyl1A0Dgi1A=", "requires": { "es6-promise": "3.2.1", - "mongodb-core": "2.1.15", + "mongodb-core": "2.1.17", "readable-stream": "2.2.7" }, "dependencies": { @@ -2498,29 +2503,29 @@ } }, "mongodb-core": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.15.tgz", - "integrity": "sha1-hB9TuH//9MdFgYnDXIroJ+EWl2Q=", + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.17.tgz", + "integrity": "sha1-pBizN6FKFJkPtRC5I97mqBMXPfg=", "requires": { "bson": "1.0.4", "require_optional": "1.0.1" } }, "mongoose": { - "version": "4.11.12", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.11.12.tgz", - "integrity": "sha512-41odmaImJVAoY/qg6gZ1Dn60qvFHIny01c7N58OlNPHBifzT6qWuiArtev7OYYE1ssPY7YvoX1hCbvZEUZfPnQ==", + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.12.4.tgz", + "integrity": "sha512-3QTbQ/+wRe8Lr1IGTBAu2gyx+7VgvnCGmY2N1HSW8nsvfMGO0QW9iNDzZT3ceEY2Wctlt28wNavHivcLDO76bQ==", "requires": { "async": "2.1.4", "bson": "1.0.4", "hooks-fixed": "2.0.0", "kareem": "1.5.0", - "mongodb": "2.2.31", + "mongodb": "2.2.33", "mpath": "0.3.0", "mpromise": "0.5.5", - "mquery": "2.3.1", + "mquery": "2.3.2", "ms": "2.0.0", - "muri": "1.2.2", + "muri": "1.3.0", "regexp-clone": "0.0.1", "sliced": "1.0.1" }, @@ -2561,24 +2566,16 @@ "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" }, "mquery": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.1.tgz", - "integrity": "sha1-mrNnSXFIAP8LtTpoHOS8TV8HyHs=", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.2.tgz", + "integrity": "sha512-KXWMypZSvhCuqRtza+HMQZdYw7PfFBjBTFvP31NNAq0OX0/NTIgpcDpkWQ2uTxk6vGQtwQ2elhwhs+ZvCA8OaA==", "requires": { - "bluebird": "2.10.2", - "debug": "2.6.8", + "bluebird": "3.5.1", + "debug": "2.6.9", "regexp-clone": "0.0.1", "sliced": "0.0.5" }, "dependencies": { - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "requires": { - "ms": "2.0.0" - } - }, "sliced": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", @@ -2592,9 +2589,9 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "muri": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/muri/-/muri-1.2.2.tgz", - "integrity": "sha1-YxmBMmUNsIoEzHnM0A3Tia/SYxw=" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/muri/-/muri-1.3.0.tgz", + "integrity": "sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg==" }, "mv": { "version": "2.1.1", @@ -3070,6 +3067,26 @@ "set-immediate-shim": "1.0.1" } }, + "redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", + "requires": { + "double-ended-queue": "2.1.0-0", + "redis-commands": "1.3.1", + "redis-parser": "2.6.0" + } + }, + "redis-commands": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz", + "integrity": "sha1-gdgm9F+pyLIBH0zXoP5ZfSQdRCs=" + }, + "redis-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" + }, "regex-cache": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", diff --git a/package.json b/package.json index 8a874a0..49c48f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jooger.me-server", - "version": "1.0.0", + "version": "1.1.0", "private": true, "scripts": { "dev": "cross-env NODE_ENV=development nodemon bin/www", @@ -40,7 +40,6 @@ "koa-compose": "^4.0.0", "koa-compress": "^2.0.0", "koa-json": "^2.0.2", - "koa-jwt": "^3.2.2", "koa-logger": "^2.0.1", "koa-onerror": "^1.2.1", "koa-passport": "^4.0.0", @@ -48,9 +47,10 @@ "koa-session": "^5.5.0", "lodash": "^4.17.4", "marked": "^0.3.6", - "mongoose": "^4.11.12", + "mongoose": "^4.12.4", "mongoose-paginate": "^5.0.3", "passport-github": "^1.1.0", + "redis": "^2.8.0", "simple-netease-cloud-music": "^0.1.8" }, "devDependencies": { diff --git a/server/app.js b/server/app.js index 1604042..8f5c45b 100644 --- a/server/app.js +++ b/server/app.js @@ -22,7 +22,10 @@ const config = require('./config') const app = new Koa() // connect mongodb -require('./mongo')() +require('./mongo').connect() + +// connect redis +require('./redis').connect() // load custom validations bouncer.Validator = require('./validation') @@ -52,4 +55,7 @@ app.use(compress()) // routes require('./routes')(app) +// +require('./service/crontab').start() + module.exports = app diff --git a/server/config/index.js b/server/config/index.js index 6c5cc70..802954b 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -34,13 +34,17 @@ const baseConfig = { } }, // TODO: Redis - redis: {}, + redis: { + host: '127.0.0.1', + port: 6379 + }, auth: { session: { key: 'jooger.me.token', maxAge: 60000 * 60 * 24 * 7, signed: false }, + userCookieKey: 'jooger.me.userid', secrets: `${packageInfo.name} ${packageInfo.version}`, defaultName: 'Jooger', defaultPassword: 'admin_jooger', diff --git a/server/controller/article.js b/server/controller/article.js index f6f8726..a526ca5 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -8,7 +8,8 @@ const config = require('../config') const { ArticleModel, CategoryModel, TagModel } = require('../model') -const { marked, isObjectId, createObjectId } = require('../util') +const { marked, isObjectId, createObjectId, getDebug } = require('../util') +const debug = getDebug('Article') exports.list = async (ctx, next) => { const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the "per_page" parameter should be greater than 0').val() diff --git a/server/controller/auth.js b/server/controller/auth.js index 264f24c..e75e15a 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -10,8 +10,8 @@ const jwt = require('jsonwebtoken') const passport = require('koa-passport') const config = require('../config') const { UserModel } = require('../model') -const { bhash, bcompare, setDebug, signToken } = require('../util') -const debug = setDebug('auth:github') +const { bhash, bcompare, getDebug, signToken } = require('../util') +const debug = getDebug('Github:Auth') const { githubPassport } = require('../service') githubPassport.init(UserModel, config) @@ -42,8 +42,8 @@ exports.localLogin = async (ctx, next) => { name: user.name }) ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - ctx.cookies.set('jooger.me.userid', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - debug.success('login success, user: ', user._id) + ctx.cookies.set(config.auth.userCookieKey, user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) + debug.success('登录成功, 用户ID:%s,用户名:%s', user._id, user.name) ctx.success({ id: user._id, token @@ -63,8 +63,8 @@ exports.logout = async (ctx, next) => { name: ctx._user.name }, false) ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) - ctx.cookies.set('jooger.me.userid', ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) - debug.success('logout success, user: ', ctx._user._id) + ctx.cookies.set(config.auth.userCookieKey, ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) + debug.success('登出成功, 用户ID:%s,用户名:%s', user._id, user.name) ctx.success(null, 'logout success') } @@ -96,7 +96,7 @@ exports.githubLogin = async (ctx, next) => { await passport.authenticate('github', { session: false }, (err, user) => { - debug('github auth callback start') + debug('Github权限验证回调处理开始') const redirectUrl = ctx.session.passport.redirectUrl const cookieDomain = config.auth.session.domain || null @@ -106,10 +106,10 @@ exports.githubLogin = async (ctx, next) => { name: user.name }) ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - ctx.cookies.set('jooger.me.userid', user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) + ctx.cookies.set(config.auth.userCookieKey, user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - debug('github auth callback finish') - debug.success('github login success, userid: ', user._id, 'username: ', user.name) + debug('Github权限验证回调处理成功') + debug.success('Github权限验证回调处理成功, 用户ID:%s,用户名:%s', user._id, user.name) return ctx.redirect(redirectUrl) })(ctx) } diff --git a/server/controller/music.js b/server/controller/music.js index d0a6e2c..0874638 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -10,15 +10,16 @@ const NeteseMusic = require('simple-netease-cloud-music') const config = require('../config') const { fetchNE } = require('../service') const { OptionModel } = require('../model') -const { proxy, setDebug } = require('../util') +const { proxy, getDebug } = require('../util') +const redis = require('../redis') const isProd = process.env.NODE_ENV === 'production' -const debug = setDebug('music') +const debug = getDebug('Music') const neteaseMusic = new NeteseMusic() - -const songListMap = {} +const cacheKey = 'musicData' exports.list = async (ctx, next) => { + // 后台实时获取 if (ctx._isAuthenticated) { const playListId = ctx.validateQuery('play_list_id') .required('the "play_list_id" parameter is required') @@ -38,24 +39,24 @@ exports.list = async (ctx, next) => { return ctx.fail() } - const musicId = option.musicId + const playListId = option.musicId + const musicData = await redis.get(cacheKey) - if (songListMap[musicId] && songListMap[musicId].length) { - return ctx.success(songListMap[musicId]) + if (musicData && musicData.id && musicData.list) { + return ctx.success(musicData.list) } - const data = await fetchSonglist(musicId) - songListMap[musicId] = data + const data = await exports.updateMusicCache(playListId) ctx.success(data) } } exports.item = async (ctx, next) => { const songId = ctx.validateParam('song_id') - .required('the "song_id" parameter is required') - .notEmpty() - .isString('the "song_id" parameter should be String type') - .val() + .required('the "song_id" parameter is required') + .notEmpty() + .isString('the "song_id" parameter should be String type') + .val() const { songs } = await neteaseMusic.song(songId) @@ -98,66 +99,61 @@ exports.cover = async (ctx, next) => { ctx.success(data) } -async function fetchSonglist (playListId) { - return fetchNE('playlist', playListId).then(({ playlist }) => { - return Promise.all( - !playlist ? [] : playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { - return Promise.all([ - neteaseMusic.url(id), - neteaseMusic.lyric(id) - ]) - .then(([song, lyric]) => [song.data[0] || null, (lyric.nolyric || !lyric.lrc) ? '' : lyric.lrc.lyric]) - .then(([song, lyric]) => { - return { - id, - name, - duration: dt || 0, - album: al && { - name: al.name, - cover: isProd ? (al.picUrl ? `${config.site}${proxy(al.picUrl)}` : '') : al.picUrl, - tns: al.tns - } || {}, - artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], - tns: tns || [], - src: isProd ? (song && song.url ? `${config.site}${proxy(song.url)}` : '') : song.url, - lyric - } - }) +// 获取除了歌曲链接和歌词外其他信息 +function fetchSonglist (playListId) { + return neteaseMusic.playlist(playListId).then(({ playlist }) => { + return !playlist ? [] : playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { + return { + id, + name, + duration: dt || 0, + album: al && { + name: al.name, + cover: isProd ? (al.picUrl ? `${config.site}${proxy(al.picUrl)}` : '') : al.picUrl, + tns: al.tns + } || {}, + artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], + tns: tns || [] } - )) + }) }).catch(err => { - debug.error(err.message) + debug.error('歌单列表获取失败,错误:', err.message) return [] }) } // 更新song list cache -exports.updateSongListMap = async function () { - debug('timed update music...') - - const option = await OptionModel.findOne({}).exec().catch(err => { - debug.error(err.message) - return null - }) - - if (option && option.musicId) { - songListMap[option.musicId] = null - } else { - debug('music playlist id is not found') +let lock = false +exports.updateMusicCache = async function (playListId = '') { + if (lock) { + debug.warn('缓存更新中...') return } - - const ids = Object.keys(songListMap) - const list = await Promise.all(ids.map(playListId => fetchSonglist(playListId))) - .catch(err => debug.error('timed update music failed, err: ', err.message)) - - if (list && list.length === ids.length) { - ids.map((id, index) => { - songListMap[id] = list[index] + lock = true + if (!playListId) { + const option = await OptionModel.findOne({}).exec().catch(err => { + debug.error('Option查找失败,错误:', err.message) + return null }) - debug.success('timed update music success...') + + if (!option || !option.musicId) { + return debug.warn('歌单ID未配置') + } + playListId = option.musicId } -} -// 每10分钟更新一次 -setInterval(exports.updateSongListMap, 1000 * 60 * 10) + const data = await fetchSonglist(playListId) + const set = { + id: playListId, + list: data + } + + redis.set(cacheKey, set).then(() => { + debug.success('缓存更新成功') + }).catch(err => { + debug.error('缓存更新失败,错误:', err.message) + }) + + lock = false + return set +} diff --git a/server/controller/option.js b/server/controller/option.js index c7bb460..b16c70e 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -8,11 +8,12 @@ const { OptionModel } = require('../model') const { getGithubUsersInfo } = require('../service') -const { updateSongListMap } = require('./music') -const debug = require('../util').setDebug('option') +const { updateMusicCache } = require('./music') +const debug = require('../util').getDebug('Option') exports.data = async (ctx, next) => { const data = await OptionModel.findOne().exec().catch(err => { + debug.error('查找失败,错误:', err.message) ctx.log.error(err.message) return null }) @@ -27,7 +28,7 @@ exports.data = async (ctx, next) => { exports.update = async (ctx, next) => { const option = ctx.request.body - const data = await exports.updateOption(option) + const data = await exports.updateOptionLinks(option) if (data) { ctx.success(data) @@ -36,10 +37,17 @@ exports.update = async (ctx, next) => { } } -exports.updateOption = async function (option = null) { - debug('timed update option...') +// update lock +let lock = false +exports.updateOptionLinks = async function (option = null) { + if (lock) { + debug.warn('友链更新中...') + return + } + lock = true if (!option) { option = await OptionModel.findOne().exec().catch(err => { + debug.error('数据查找失败,错误:', err.message) ctx.log.error(err.message) return {} }) @@ -49,16 +57,15 @@ exports.updateOption = async function (option = null) { option.links = await generateLinks(option.links) const data = await OptionModel.findOneAndUpdate({}, option, { new: true }).exec().catch(err => { + debug.error('数据更新失败,错误:', err.message) ctx.log.error(err.message) return null }) - // 更新 music list - await updateSongListMap() - if (data) { - debug.success('timed update option success...') + debug.success('友链更新成功') } + lock = false return data } @@ -82,6 +89,3 @@ async function generateLinks (links = []) { } return links } - -// 每1小时更新一次 -setInterval(exports.updateOption, 1000 * 60 * 60 * 1) diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index 7c5f3fe..3e7501b 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -7,29 +7,15 @@ 'use strict' const compose = require('koa-compose') -const koajwt = require('koa-jwt') const jwt = require('jsonwebtoken') const passport = require('koa-passport') const config = require('../config') const { UserModel } = require('../model') -const debug = require('../util').setDebug('auth') +const debug = require('../util').getDebug('Auth') const isProd = process.env.NODE_ENV === 'production' -// 开发环境下,请求携带_DEV_参数,视为已验证 -function devAuth () { - return async (ctx, next) => { - if (!isProd && ctx.query._DEV_) { - ctx.session._verify = true - } - await next() - } -} - function verifyToken () { return async (ctx, next) => { - if (ctx._devauth) { - return await next() - } ctx.session._verify = false const token = ctx.cookies.get(config.auth.session.key) @@ -38,13 +24,15 @@ function verifyToken () { try { decodedToken = await jwt.verify(token, config.auth.secrets) } catch (err) { - ctx.fail(401) + debug.error('Token校验出错,错误:', err.message) + return ctx.fail(401) } if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { - // 已验证权限 + // 已校验权限 ctx.session._verify = true ctx.session._token = token + debug.success('Token校验成功') } } await next() @@ -53,21 +41,20 @@ function verifyToken () { exports.isAuthenticated = () => { return compose([ - devAuth(), verifyToken(), async (ctx, next) => { if (!ctx.session._verify) { return ctx.fail(401) } - const userId = ctx.cookies.get('jooger.me.userid', { signed: false }) - + const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) const user = await UserModel.findById(userId).exec().catch(err => { + debug.error('用户查找失败, 错误:', err.message) ctx.log.error(err.message) return null }) if (!user) { - return ctx.fail(401, 'the user was not found') + return ctx.fail(401, '用户不存在') } ctx._user = user ctx._isAuthenticated = true @@ -83,7 +70,7 @@ exports.snsAuth = (name = '') => { // 如果已经登录 const redirectUrl = ctx.query.redirectUrl || config.site if (ctx.session._verify) { - debug.info('you have already logged in, redirecting') + debug.info('您已经登录, 重定向中...') return ctx.redirect(redirectUrl) } ctx.session.passport = { redirectUrl } @@ -100,7 +87,7 @@ exports.snsLogout = () => compose([ verifyToken(), async (ctx, next) => { if (!ctx.session._verify) { - return ctx.fail(-1, 'please login first') + return ctx.fail(-1, '请您先登录') } await next() } diff --git a/server/middleware/error.js b/server/middleware/error.js index 8a2b55f..265b31c 100644 --- a/server/middleware/error.js +++ b/server/middleware/error.js @@ -10,6 +10,7 @@ module.exports = async (ctx, next) => { try { await next() } catch (err) { + // TODO: 错误日志钩子 let code = err.status || 500 if (err.name === 'ValidationError') { diff --git a/server/middleware/formidable.js b/server/middleware/formidable.js index b9a6c07..96a784b 100644 --- a/server/middleware/formidable.js +++ b/server/middleware/formidable.js @@ -1,5 +1,5 @@ /** - * @desc Formidable + * @desc Formidable 上传中间件,暂未使用 * @author Jooger * @date 10 Oct 2017 */ diff --git a/server/mongo.js b/server/mongo.js index 313fbf3..abb9c0d 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc Mongodb connect * @author Jooger * @date 25 Sep 2017 */ @@ -9,21 +9,32 @@ const mongoose = require('mongoose') const config = require('./config') const { UserModel, OptionModel } = require('./model') -const { updateOption } = require('./controller/option') -const { bhash, setDebug } = require('./util') -const debug = setDebug('mongo:connect') +const { bhash, getDebug } = require('./util') +const debug = getDebug('MongoDB') +let isConnected = false -module.exports = function () { - mongoose.Promise = global.Promise +mongoose.Promise = global.Promise + +exports.connect = () => { mongoose.connect(config.mongo.uri, config.mongo.option, err => { if (err) { - debug.error('connect to %s error: ', config.mongo.uri, err.message) + isConnected = false + debug.error('连接失败,错误: ', config.mongo.uri, err.message) process.exit(0) } + debug.success('连接成功') + isConnected = true + seed() }) +} + +exports.seed = seed - seedOption() - seedAdmin() +function seed () { + if (isConnected) { + seedOption() + seedAdmin() + } } // 参数初始化 @@ -33,8 +44,6 @@ async function seedOption () { if (!option) { await new OptionModel().save().catch(err => debug.error(err.message)) } - - updateOption() } // 管理员初始化 @@ -43,7 +52,7 @@ function seedAdmin () { if (!data) { createAdmin() } - }) + }).catch(err => debug.error(err.message)) function createAdmin () { new UserModel({ diff --git a/server/redis.js b/server/redis.js new file mode 100644 index 0000000..9d0a2e3 --- /dev/null +++ b/server/redis.js @@ -0,0 +1,70 @@ +/** + * @desc Redis connect + * @author Jooger + * @date 27 Oct 2017 + */ + +'use strict' + +const redis = require('redis') +const config = require('./config') +const { getDebug, isType } = require('./util') +const debug = getDebug('Redis') +let client = null +const cache = {} + +exports.connect = () => { + if (client) { + return debug('已连接') + } + client = redis.createClient(config.redis) + client.on('error', err => { + debug.error('连接失败, 错误: ', err.message) + client = null + }) + client.on('connect', () => debug.success('连接成功')) + client.on('reconnecting', () => debug('正在重连中...')) +} + +exports.set = (key = '', value = '') => new Promise((resolve, reject) => { + if (client) { + if (!isType(value, 'String')) { + try { + value = JSON.stringify(value) + } catch (err) { + debug.error('存储时,序列化失败, 错误:%s', err.message) + value = value.toString() + } + } + client.set(key, value, (err, res) => { + if (err) { + debug.error('存储【 %s 】失败,错误:%s', key, err.message) + return reject(err) + } + resolve(res) + }) + } else { + cache[key] = value + resolve(value) + } +}) + + +exports.get = (key = '') => new Promise((resolve, reject) => { + if (client) { + client.get(key, (err, res) => { + if (err) { + debug.error('读取【 %s 】失败,错误:%s', key, err.message) + return reject(err) + } + try { + res = JSON.parse(res) + } catch (err) { + debug.error('获取时,序列化失败, 错误:%s', err.message) + } + resolve(res) + }) + } else { + resolve(cache[key]) + } +}) diff --git a/server/routes/backend.js b/server/routes/backend.js index d029543..5c77207 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -45,6 +45,9 @@ router.delete('/users/:id', isAuthenticated, user.delete) // Music router.get('/music/songs', isAuthenticated, music.list) +router.get('/music/songs/:song_id', isAuthenticated, music.item) +router.get('/music/songs/:song_id/url', isAuthenticated, music.url) +router.get('/music/songs/:song_id/lyric', isAuthenticated, music.lyric) // Auth router.get('/auth/local/logout', isAuthenticated, auth.logout) diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 6ac9cea..f941804 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -29,9 +29,8 @@ router.get('/categories/:id', category.item) // Music router.get('/music/songs', music.list) router.get('/music/songs/:song_id', music.item) -// router.get('/music/songs/:song_id/url', music.url) -// router.get('/music/songs/:song_id/lyric', music.lyric) -// router.get('/music/songs/cover/:cover_id', music.cover) +router.get('/music/songs/:song_id/url', music.url) +router.get('/music/songs/:song_id/lyric', music.lyric) // Option router.get('/options', option.data) diff --git a/server/service/crontab.js b/server/service/crontab.js new file mode 100644 index 0000000..757e1bb --- /dev/null +++ b/server/service/crontab.js @@ -0,0 +1,18 @@ +/** + * @desc 定时任务 + * @author Jooger + * @date 27 Oct 2017 + */ + +'use strict' + +exports.start = () => { + const { option, music } = require('../controller') + // 友链 每1小时更新一次 + option.updateOptionLinks() + setInterval(option.updateOptionLinks.bind(option), 1000 * 60 * 60 * 1) + + // 音乐 每10分钟更新一次 + music.updateMusicCache() + setInterval(music.updateMusicCache.bind(music), 1000 * 60 * 10) +} diff --git a/server/service/github-passport.js b/server/service/github-passport.js index 420b0bc..947f2b6 100644 --- a/server/service/github-passport.js +++ b/server/service/github-passport.js @@ -10,8 +10,8 @@ const passport = require('koa-passport') const GithubStrategy = require('passport-github').Strategy const config = require('../config') const { clientID, clientSecret, callbackURL } = config.sns.github -const { randomString, setDebug } = require('../util') -const debug = setDebug('auth:github') +const { randomString, getDebug } = require('../util') +const debug = getDebug('Github:Auth') exports.init = (UserModel, config) => { passport.use(new GithubStrategy({ @@ -20,12 +20,12 @@ exports.init = (UserModel, config) => { callbackURL, passReqToCallback: true }, async (req, accessToken, refreshToken, profile, done) => { - debug('github auth start') + debug('Github权限验证开始...') try { const user = await UserModel.findOne({ 'github.id': profile.id }).catch(err => { - debug.error('user check error, err: ', err.message) + debug.error('本地用户查找失败, 错误:', err.message) return null }) @@ -41,7 +41,7 @@ exports.init = (UserModel, config) => { userData.github.token = accessToken const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData).exec().catch(err => { - debug.error('user update error, err: ', err.message) + debug.error('本地用户更新失败, 错误:', err.message) }) || user return end(null, updatedUser) @@ -58,7 +58,7 @@ exports.init = (UserModel, config) => { newUser.github.token = accessToken const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { - debug.error('user check error, err: ', err.message) + debug.error('本地用户查找失败, 错误:', err.message) return true }) @@ -67,17 +67,17 @@ exports.init = (UserModel, config) => { } const data = await new UserModel(newUser).save().catch(err => { - debug.error('user create fail, err: ', err.message) + debug.error('本地用户创建失败, 错误:', err.message) }) return end(null, data) } catch (err) { - debug.error('github auth error') + debug.error('Github权限验证失败,错误:', err) return end(err) } function end (err, data) { - debug('github auth finish') + debug.success('Github权限验证成功') done(err, data) } })) diff --git a/server/service/github-userinfo.js b/server/service/github-userinfo.js index 9d15cc3..9a6b3f3 100644 --- a/server/service/github-userinfo.js +++ b/server/service/github-userinfo.js @@ -7,10 +7,10 @@ 'use strict' const axios = require('axios') -const { setDebug } = require('../util') +const { getDebug } = require('../util') const config = require('../config') const { clientID, clientSecret } = config.sns.github -const debug = setDebug('github:user') +const debug = getDebug('Github:User') const getGithubUsersInfo = (githubNames = '') => { if (!githubNames) { @@ -22,7 +22,6 @@ const getGithubUsersInfo = (githubNames = '') => { } const task = githubNames.map(name => { - debug('fetch github user [', name, ']') return axios.get(`https://api.github.com/users/${name}`, { params: { client_id: clientID, @@ -30,14 +29,13 @@ const getGithubUsersInfo = (githubNames = '') => { } }).then(res => { if (res && res.status === 200) { - debug.success('fetch github user success [', name, ']') + debug.success('抓取【 %s 】信息成功', name,) return res.data } return null }) .catch(err => { - console.error(err) - debug.error(err.message) + debug.error('抓取【 %s 】信息失败,错误:%s', name, err.message) return null }) }) diff --git a/server/service/index.js b/server/service/index.js index 1d6b2d9..473f105 100644 --- a/server/service/index.js +++ b/server/service/index.js @@ -7,7 +7,6 @@ 'use strict' exports.githubPassport = require('./github-passport') - exports.getGithubUsersInfo = require('./github-userinfo') - exports.fetchNE = require('./netease-music') +exports.crontab = require('./crontab') diff --git a/server/service/netease-music.js b/server/service/netease-music.js index 1a5a762..ba59d8f 100644 --- a/server/service/netease-music.js +++ b/server/service/netease-music.js @@ -7,8 +7,8 @@ 'use strict' const axios = require('axios') -const { encrypt, setDebug } = require('../util') -const debug = setDebug('netease') +const { encrypt, getDebug } = require('../util') +const debug = getDebug('Netease') const neFetcher = axios.create({ baseURL: 'http://music.163.com', diff --git a/server/util/debug.js b/server/util/debug.js index 84957b5..b6655c0 100644 --- a/server/util/debug.js +++ b/server/util/debug.js @@ -17,7 +17,7 @@ const levelMap = { } const slice = Array.prototype.slice -module.exports = function setDebug (namespace = '') { +module.exports = function getDebug (namespace = '') { const deBug = debug(`[${packageInfo.name}]${namespace ? ' ' + namespace : ''}`) function d () { diff --git a/server/util/index.js b/server/util/index.js index 1b038b0..67e3ca0 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -9,7 +9,7 @@ const bcrypt = require('bcryptjs') const mongoose = require('mongoose') -exports.setDebug = require('./debug') +exports.getDebug = require('./debug') exports.signToken = require('./sign-token') @@ -19,6 +19,20 @@ exports.encrypt = require('./encrypt') exports.proxy = require('./proxy') +exports.noop = function () {} + +exports.isType = (obj = {}, type = 'Object') => { + if (!Array.isArray(type)) { + type = [type] + } + return type.some(t => { + if (typeof t !== 'string') { + return false + } + return Object.prototype.toString.call(obj) === `[object ${t}]` + }) +} + exports.createObjectId = () => mongoose.Types.ObjectId() exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) diff --git a/server/util/proxy.js b/server/util/proxy.js index 94e614c..037bf6f 100644 --- a/server/util/proxy.js +++ b/server/util/proxy.js @@ -1,5 +1,5 @@ /** - * @desc Http url proxy replace + * @desc Http url replace to "/proxy/..." * @author Jooger * @date 20 Oct 2017 */ From 116f83b6101da739e6500bf3344fb1d603b3fb38 Mon Sep 17 00:00:00 2001 From: Jooger Date: Fri, 27 Oct 2017 19:33:21 +0800 Subject: [PATCH 045/208] [update] update mongodb reconnect strategy --- server/config/index.js | 7 +++++-- server/mongo.js | 10 ++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index 802954b..55df03f 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -30,10 +30,13 @@ const baseConfig = { mongo: { option: { useMongoClient: true, - poolSize: 20 + poolSize: 20, + keepAlive: true, + autoReconnect: true, + reconnectInterval: 1000, + reconnectTries: Number.MAX_VALUE } }, - // TODO: Redis redis: { host: '127.0.0.1', port: 6379 diff --git a/server/mongo.js b/server/mongo.js index abb9c0d..d054476 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -16,15 +16,13 @@ let isConnected = false mongoose.Promise = global.Promise exports.connect = () => { - mongoose.connect(config.mongo.uri, config.mongo.option, err => { - if (err) { - isConnected = false - debug.error('连接失败,错误: ', config.mongo.uri, err.message) - process.exit(0) - } + mongoose.connect(config.mongo.uri, config.mongo.option).then(() => { debug.success('连接成功') isConnected = true seed() + }, err => { + isConnected = false + return debug.error('连接失败,错误: ', config.mongo.uri, err.message) }) } From 62fccb580b8fffc059ab6dca55511a76de9f2d2e Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 28 Oct 2017 18:45:20 +0800 Subject: [PATCH 046/208] [update] update article api, add populating category --- package.json | 1 - server/controller/article.js | 22 +++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 49c48f3..9a38c8f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ }, "bin": "./node_modules/.bin/", "dependencies": { - "aliyun-sdk": "^1.10.18", "axios": "^0.16.2", "bcryptjs": "^2.4.3", "big-integer": "^1.6.25", diff --git a/server/controller/article.js b/server/controller/article.js index a526ca5..5cc7946 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -339,7 +339,11 @@ async function getRelatedArticles (ctx, data) { state: 1, tag: { $in: tag.map(t => t._id) }} ) - .select('title thumb createdAt publishedAt meta') + .select('title thumb createdAt publishedAt meta category') + .populate({ + path: 'category', + select: 'name description' + }) .exec() .catch(err => { ctx.log.error('related articles access failed, err: ', err.message) @@ -347,8 +351,8 @@ async function getRelatedArticles (ctx, data) { }) if (articles) { - // 取前5篇 - data.related = articles.slice(0, 5) + // 取前10篇 + data.related = articles.slice(0, 10) } } @@ -365,7 +369,11 @@ async function getSiblingArticles (ctx, data) { query.state = 1 } let prev = await ArticleModel.findOne(query) - .select('title createdAt publishedAt thumb') + .select('title createdAt publishedAt thumb category') + .populate({ + path: 'category', + select: 'name description' + }) .sort('-createdAt') .lt('createdAt', data.createdAt) .exec() @@ -374,7 +382,11 @@ async function getSiblingArticles (ctx, data) { return null }) let next = await ArticleModel.findOne(query) - .select('title createdAt publishedAt thumb') + .select('title createdAt publishedAt thumb category') + .populate({ + path: 'category', + select: 'name description' + }) .sort('createdAt') .gt('createdAt', data.createdAt) .exec() From ef703702fdeef57cdacfcb4102f92d64b3781b73 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 29 Oct 2017 00:32:06 +0800 Subject: [PATCH 047/208] [feature] add comment api --- README.md | 6 + ecosystem.config.js | 4 +- package-lock.json | 683 +++++++++++++++++------------- package.json | 11 +- server/config/index.js | 12 +- server/config/production.js | 3 + server/controller/article.js | 32 +- server/controller/comment.js | 387 +++++++++++++++++ server/controller/index.js | 1 + server/middleware/authenticate.js | 2 +- server/middleware/response.js | 6 + server/model/schema/comment.js | 23 +- server/model/schema/option.js | 2 +- server/routes/backend.js | 16 +- server/routes/frontend.js | 8 +- 15 files changed, 861 insertions(+), 335 deletions(-) create mode 100644 server/controller/comment.js diff --git a/README.md b/README.md index 8fbafc7..47308a0 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ My blog's api server build by koa2 and mongoose * ~~Redis缓存部分数据~~ (2017.10.27 v1.1) +* 垃圾评论过滤 [akismet](https://github.com/chrisfosterelli/akismet-api) + +* 评论定位 [geoip](https://github.com/bluesmoon/node-geoip) + +* 评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer) + * 评论api * 消息api diff --git a/ecosystem.config.js b/ecosystem.config.js index 54b0a5e..21dd9e5 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -13,7 +13,7 @@ module.exports = { name: packageInfo.name, script: './bin/www', cwd: __dirname, - watch: true, + watch: false, ignore_watch: ['[\/\\]\./', 'node_modules'], env: { NODE_ENV: 'production' @@ -33,7 +33,7 @@ module.exports = { ref : 'origin/master', repo : packageInfo.repository.url, path : '/root/www/' + packageInfo.name, - 'post-deploy' : 'git pull && cnpm install && pm2 stop all && pm2 startOrReload ecosystem.config.js && pm2 start all' + 'post-deploy' : 'git pull && cnpm install && pm2 startOrReload ecosystem.config.js' } } } diff --git a/package-lock.json b/package-lock.json index 00d42ba..48dd543 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jooger.me-server", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -19,16 +19,13 @@ "negotiator": "0.6.1" } }, - "aliyun-sdk": { - "version": "1.10.18", - "resolved": "https://registry.npmjs.org/aliyun-sdk/-/aliyun-sdk-1.10.18.tgz", - "integrity": "sha1-3OLLNdO+r1Js5CxibGJAPsl0QWg=", + "akismet-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/akismet-api/-/akismet-api-3.0.0.tgz", + "integrity": "sha512-Kg0tgNFa8PX85cwZ+KinKfs/NDbRmXu7TZ/3mu4z/tBAwJosiqATEzmOGJLLDWfiXK01qHQJh6Ro6qRWpMp6Dg==", "requires": { - "node_memcached": "1.1.3", - "pomelo-protobuf": "0.4.0", - "protobufjs": "4.1.3", - "xml2js": "0.4.4", - "xmlbuilder": "2.6.5" + "bluebird": "3.5.1", + "superagent": "3.8.0" } }, "amdefine": { @@ -91,15 +88,6 @@ "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", "dev": true }, - "ascli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", - "integrity": "sha1-vPpZdKYvGOgcq660lzKrSoj5Brw=", - "requires": { - "colour": "0.7.1", - "optjs": "3.2.2" - } - }, "async": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", @@ -111,6 +99,11 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, "axios": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", @@ -148,6 +141,15 @@ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.25.tgz", "integrity": "sha1-HeRan1dUKsIBIcaC+NZCIgo06CM=" }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "requires": { + "buffers": "0.1.1", + "chainsaw": "0.1.0" + } + }, "binary-extensions": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", @@ -246,10 +248,10 @@ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" }, - "bufferview": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bufferview/-/bufferview-1.0.1.tgz", - "integrity": "sha1-ev10pF+Tf6QiodM4wIu/3HbNcl0=" + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" }, "bunyan": { "version": "1.5.1", @@ -261,15 +263,6 @@ "safe-json-stringify": "1.0.4" } }, - "bytebuffer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-4.1.0.tgz", - "integrity": "sha1-TFgmngUqseSx9/82T9+zzogpBqo=", - "requires": { - "bufferview": "1.0.1", - "long": "2.4.0" - } - }, "bytes": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", @@ -291,6 +284,14 @@ "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", "dev": true }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "requires": { + "traverse": "0.3.9" + } + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -326,36 +327,6 @@ "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", "dev": true }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -375,7 +346,8 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true }, "color-convert": { "version": "1.9.0", @@ -392,10 +364,23 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, - "colour": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", - "integrity": "sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g=" + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" }, "compressible": { "version": "2.0.11", @@ -434,6 +419,11 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=" + }, "cookies": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.1.tgz", @@ -523,6 +513,11 @@ "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", "dev": true }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -656,6 +651,11 @@ "fill-range": "2.2.3" } }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, "extglob": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", @@ -707,6 +707,16 @@ "for-in": "1.0.2" } }, + "form-data": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, "formidable": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.1.1.tgz", @@ -723,6 +733,11 @@ "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", "dev": true }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, "fsevents": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", @@ -1622,6 +1637,72 @@ } } }, + "fstream": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-0.1.31.tgz", + "integrity": "sha1-czfwWPu7vvqMn1YaKMqwhJICyYg=", + "requires": { + "graceful-fs": "3.0.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.4.5" + }, + "dependencies": { + "graceful-fs": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "requires": { + "natives": "1.1.0" + } + } + } + }, + "geoip-lite": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/geoip-lite/-/geoip-lite-1.2.1.tgz", + "integrity": "sha1-OcSO+T5JiiWezka8npjPPdtoDYs=", + "requires": { + "async": "2.5.0", + "colors": "1.1.2", + "glob": "7.1.2", + "iconv-lite": "0.4.13", + "lazy": "1.0.11", + "rimraf": "2.6.2", + "unzip": "0.1.11" + }, + "dependencies": { + "async": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", + "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "requires": { + "lodash": "4.17.4" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + } + } + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -1632,7 +1713,6 @@ "version": "6.0.4", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "optional": true, "requires": { "inflight": "1.0.6", "inherits": "2.0.3", @@ -1704,11 +1784,6 @@ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=" }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, "hooks-fixed": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz", @@ -1787,11 +1862,6 @@ "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", "dev": true }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -1930,11 +2000,6 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, - "isemail": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", - "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1955,29 +2020,23 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "joi": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", - "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", - "requires": { - "hoek": "2.16.3", - "isemail": "1.2.0", - "moment": "2.18.1", - "topo": "1.1.0" - } - }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "jsonwebtoken": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.1.tgz", - "integrity": "sha1-fKMk9SFfi+A5zTWmxFu4y3SkSPs=", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz", + "integrity": "sha1-xjl80uX9WD1lwAeoPce7eOaYK4M=", "requires": { - "joi": "6.10.1", "jws": "3.1.4", + "lodash.includes": "4.3.0", + "lodash.isboolean": "3.0.3", + "lodash.isinteger": "4.0.4", + "lodash.isnumber": "3.0.3", + "lodash.isplainobject": "4.0.6", + "lodash.isstring": "4.0.1", "lodash.once": "4.1.1", "ms": "2.0.0", "xtend": "4.0.1" @@ -2147,15 +2206,6 @@ "streaming-json-stringify": "3.1.0" } }, - "koa-jwt": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/koa-jwt/-/koa-jwt-3.2.2.tgz", - "integrity": "sha1-aA3mFYaWeKeVg4JwGQ16RFgeM04=", - "requires": { - "jsonwebtoken": "7.4.1", - "koa-unless": "1.0.0" - } - }, "koa-logger": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/koa-logger/-/koa-logger-2.0.1.tgz", @@ -2224,11 +2274,6 @@ "uid-safe": "2.1.5" } }, - "koa-unless": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/koa-unless/-/koa-unless-1.0.0.tgz", - "integrity": "sha1-WqVzhLyIJWivyQrASFKj1YZYrus=" - }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -2238,13 +2283,10 @@ "package-json": "4.0.1" } }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "1.0.0" - } + "lazy": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz", + "integrity": "sha1-2qBoIGKCVCwIgojpdcKXwa53tpA=" }, "lodash": { "version": "4.17.4", @@ -2317,6 +2359,11 @@ "lodash.restparam": "3.6.1" } }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -2329,6 +2376,31 @@ "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", "dev": true }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, "lodash.keys": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", @@ -2351,11 +2423,6 @@ "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", "dev": true }, - "long": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", - "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=" - }, "lowercase-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", @@ -2392,6 +2459,38 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=" }, + "match-stream": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/match-stream/-/match-stream-0.0.2.tgz", + "integrity": "sha1-mesFAJOzTf+t5CG5rAtBCpz6F88=", + "requires": { + "buffers": "0.1.1", + "readable-stream": "1.0.34" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2423,6 +2522,11 @@ "regex-cache": "0.4.4" } }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, "mime-db": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", @@ -2453,7 +2557,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, "requires": { "minimist": "0.0.8" }, @@ -2461,16 +2564,10 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "optional": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, - "moment": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", - "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" - }, "mongodb": { "version": "2.2.33", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.33.tgz", @@ -2610,6 +2707,11 @@ "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=", "optional": true }, + "natives": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.0.tgz", + "integrity": "sha1-6f+EFBimsux6SV6TmYT3jxY+bjE=" + }, "ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", @@ -2621,13 +2723,10 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, - "node_memcached": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/node_memcached/-/node_memcached-1.1.3.tgz", - "integrity": "sha1-icFSr4itKIF/ANiRyZBFHV1xLqg=", - "requires": { - "debug": "2.6.9" - } + "nodemailer": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.3.1.tgz", + "integrity": "sha512-ngyDzou/Rbppn9WUOpWNoe25mRrW5wMwRokWanBNLt+3YaxOLmUtrc0ZtHMOgHGFPAYNgKA9H70ELlV3qSHL7Q==" }, "nodemon": { "version": "1.12.1", @@ -2677,7 +2776,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true }, "oauth": { "version": "0.9.15", @@ -2724,18 +2824,10 @@ "wordwrap": "0.0.3" } }, - "optjs": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", - "integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4=" - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "requires": { - "lcid": "1.0.0" - } + "over": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/over/-/over-0.0.5.tgz", + "integrity": "sha1-8phS5w/X4l82DgE6jsRMgq7bVwg=" }, "p-finally": { "version": "1.0.0", @@ -2856,11 +2948,6 @@ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, - "pomelo-protobuf": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/pomelo-protobuf/-/pomelo-protobuf-0.4.0.tgz", - "integrity": "sha1-5F6aCkRusYZn4MbhPutT1Hrdvag=" - }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -2878,73 +2965,6 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, - "protobufjs": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-4.1.3.tgz", - "integrity": "sha1-jjbRsCJsu2jWR+S0TCoUTzfyd54=", - "requires": { - "ascli": "1.0.1", - "bytebuffer": "4.1.0", - "glob": "5.0.15", - "yargs": "3.32.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "window-size": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", - "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" - }, - "yargs": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", - "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", - "requires": { - "camelcase": "2.1.1", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "os-locale": "1.4.0", - "string-width": "1.0.2", - "window-size": "0.1.4", - "y18n": "3.2.1" - } - } - } - }, "ps-tree": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", @@ -2960,6 +2980,40 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "pullstream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pullstream/-/pullstream-0.4.1.tgz", + "integrity": "sha1-1vs79a7Wl+gxFQ6xACwlo/iuExQ=", + "requires": { + "over": "0.0.5", + "readable-stream": "1.0.34", + "setimmediate": "1.0.5", + "slice-stream": "1.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "qs": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz", @@ -3156,7 +3210,6 @@ "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "optional": true, "requires": { "glob": "6.0.4" } @@ -3172,11 +3225,6 @@ "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", "optional": true }, - "sax": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", - "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=" - }, "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", @@ -3197,6 +3245,11 @@ "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", "dev": true }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, "setprototypeof": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", @@ -3228,6 +3281,37 @@ "resolved": "https://registry.npmjs.org/simple-netease-cloud-music/-/simple-netease-cloud-music-0.1.8.tgz", "integrity": "sha512-1vTRDzk0TYEUWrqdiUMl/cobJPj67YBmEaBfqqjnl+epC1ubhynqeqppy8bDIVX4giPItOWaI7ugz60rY1q+TA==" }, + "slice-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-stream/-/slice-stream-1.0.0.tgz", + "integrity": "sha1-WzO9ZvATsaf4ZGCwPUY97DmtPqA=", + "requires": { + "readable-stream": "1.0.34" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "sliced": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", @@ -3328,6 +3412,38 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, + "superagent": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.0.tgz", + "integrity": "sha512-71XGWgtn70TNwgmgYa69dPOYg55aU9FCahjUNY03rOrKvaTCaU3b9MeZmqonmf9Od96SCxr3vGfEAnhM7dtxCw==", + "requires": { + "component-emitter": "1.2.1", + "cookiejar": "2.1.1", + "debug": "3.1.0", + "extend": "3.0.1", + "form-data": "2.3.1", + "formidable": "1.1.1", + "methods": "1.1.2", + "mime": "1.4.1", + "qs": "6.5.1", + "readable-stream": "2.3.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + } + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -3363,14 +3479,6 @@ "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", "dev": true }, - "topo": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", - "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", - "requires": { - "hoek": "2.16.3" - } - }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -3380,6 +3488,11 @@ "nopt": "1.0.10" } }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" + }, "type-is": { "version": "1.6.15", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", @@ -3438,6 +3551,42 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "unzip": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/unzip/-/unzip-0.1.11.tgz", + "integrity": "sha1-iXScY7BY19kNYZ+GuYqhU107l/A=", + "requires": { + "binary": "0.3.0", + "fstream": "0.1.31", + "match-stream": "0.0.2", + "pullstream": "0.4.1", + "readable-stream": "1.0.34", + "setimmediate": "1.0.5" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "unzip-response": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", @@ -3554,35 +3703,6 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3605,40 +3725,11 @@ "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", "dev": true }, - "xml2js": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", - "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=", - "requires": { - "sax": "0.6.1", - "xmlbuilder": "2.6.5" - } - }, - "xmlbuilder": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.6.5.tgz", - "integrity": "sha1-b/etYPty0idk8AehZLd/K/FABSY=", - "requires": { - "lodash": "3.10.1" - }, - "dependencies": { - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - } - } - }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" - }, "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", diff --git a/package.json b/package.json index 9a38c8f..82e52cf 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,12 @@ "scripts": { "dev": "cross-env NODE_ENV=development nodemon bin/www", "debug": "cross-env NODE_ENV=development nodemon --inspect bin/www", + "prod": "cross-env NODE_ENV=production nodemon bin/www", "pm2": "pm2 startOrReload ecosystem.config.js", "test": "echo \"Error: no test specified\" && exit 1", "deploy": "pm2 deploy ecosystem.config.js production" }, + "site": "https://jooger.me", "repository": { "type": "https", "url": "https://github.com/jo0ger/jooger.me-server.git" @@ -16,7 +18,10 @@ "keywords": [ "jooger.me", "server", - "api" + "api", + "Nodejs", + "Koa2", + "MongoDB" ], "author": "Jooger", "license": "MIT", @@ -25,13 +30,16 @@ }, "bin": "./node_modules/.bin/", "dependencies": { + "akismet-api": "^3.0.0", "axios": "^0.16.2", "bcryptjs": "^2.4.3", "big-integer": "^1.6.25", "crypto": "^1.0.1", "debug": "^2.6.9", "formidable": "^1.1.1", + "geoip-lite": "^1.2.1", "highlight.js": "^9.12.0", + "jsonwebtoken": "^8.1.0", "koa": "^2.2.0", "koa-bodyparser": "^3.2.0", "koa-bouncer": "^6.0.0", @@ -48,6 +56,7 @@ "marked": "^0.3.6", "mongoose": "^4.12.4", "mongoose-paginate": "^5.0.3", + "nodemailer": "^4.3.1", "passport-github": "^1.1.0", "redis": "^2.8.0", "simple-netease-cloud-music": "^0.1.8" diff --git a/server/config/index.js b/server/config/index.js index 55df03f..e995f1e 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -14,11 +14,10 @@ const baseConfig = { name: packageInfo.name, version: packageInfo.version, author: packageInfo.author || 'Jooger', - site: 'https://jooger.me', + site: packageInfo.site, env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3001, - pageSize: 15, codeMap: { '-1': 'fail', '200': 'success', @@ -27,6 +26,9 @@ const baseConfig = { '500': 'server error', '10001': 'params error' }, + articleLimit: 15, + commentLimit: 99, + commentSpamLimit: 3, mongo: { option: { useMongoClient: true, @@ -65,6 +67,12 @@ const baseConfig = { clientSecret: 'github client secret', callbackURL: 'github oauth callback url' } + }, + akismet: { + apiKey: 'akismet api key', + activeSites: [ + packageInfo.site + ] } } diff --git a/server/config/production.js b/server/config/production.js index ee25598..7464684 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -23,5 +23,8 @@ module.exports = { clientSecret: '4b98cc1028eddc78e72d5e48657819be50581623', callbackURL: 'https://api.jooger.me/auth/github/login/callback' } + }, + akismet: { + apiKey: '7fa12f4a1d08' } } diff --git a/server/controller/article.js b/server/controller/article.js index 5cc7946..e8dac71 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -12,12 +12,15 @@ const { marked, isObjectId, createObjectId, getDebug } = require('../util') const debug = getDebug('Article') exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.pageSize).toInt().gt(0, 'the "per_page" parameter should be greater than 0').val() + const pageSize = ctx.validateQuery('per_page').defaultTo(config.articleLimit).toInt().gt(0, 'the "per_page" parameter should be greater than 0').val() const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'the "page" parameter should be greater than 0').val() const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() const category = ctx.validateQuery('category').optional().toString().val() const tag = ctx.validateQuery('tag').optional().toString().val() const keyword = ctx.validateQuery('keyword').optional().toString().val() + // 时间区间查询仅后台可用,且依赖于createdAt + const startDate = ctx.validateQuery('start_date').optional().toString().val() + const endDate = ctx.validateQuery('end_date').optional().toString().val() // 排序仅后台能用,且order和sortBy需同时传入才起作用 // -1 desc | 1 asc const order = ctx.validateQuery('order').optional().toInt().isIn( @@ -107,6 +110,22 @@ exports.list = async (ctx, next) => { options.sort = {} options.sort[sortBy] = order } + + // 起始日期 + if (startDate) { + const $gte = new Date(startDate) + if ($gte.toString() !== 'Invalid Date') { + query.createdAt = { $gte } + } + } + + // 结束日期 + if (endDate) { + const $lte = new Date(endDate) + if ($lte.toString() !== 'Invalid Date') { + query.createdAt = Object.assign({}, query.createdAt, { $lte }) + } + } } const articles = await ArticleModel.paginate(query, options).catch(err => { @@ -269,16 +288,7 @@ exports.update = async (ctx, next) => { article.renderedContent = marked(content) } - if (cache) { - // 如果文章状态由草稿变为发布,更新发布时间 - if (cache.state !== article.state && article.state === 1) { - article.publishedAt = Date.now() - } - } - - const data = await ArticleModel.findByIdAndUpdate(id, article, { - new: true - }) + const data = await ArticleModel.findByIdAndUpdate(id, article, { new: true }) .populate('category tag') .exec() .catch(err => { diff --git a/server/controller/comment.js b/server/controller/comment.js new file mode 100644 index 0000000..37cc509 --- /dev/null +++ b/server/controller/comment.js @@ -0,0 +1,387 @@ +/** + * @desc Comment controller + * @author Jooger + * @date 28 Oct 2017 + */ + +'use strict' + +const geoip = require('geoip-lite') +const config = require('../config') +const { CommentModel, UserModel, ArticleModel } = require('../model') +const { marked, isObjectId, createObjectId, getDebug } = require('../util') +const debug = getDebug('Comment') + +exports.list = async (ctx, next) => { + const pageSize = ctx.validateQuery('per_page').defaultTo(config.commentLimit).toInt().gt(0, '每页评论数量必须大于0').val() + const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() + const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() + const type = ctx.validateQuery('type').optional().toInt().isIn([0, 1], '评论类型参数无效').val() + const author = ctx.validateQuery('author').optional().toString().isObjectId('用户ID参数无效').val() + const article = ctx.validateQuery('article').optional().toString().isObjectId('文章ID参数无效').val() + const keyword = ctx.validateQuery('keyword').optional().toString().val() + // 时间区间查询仅后台可用,且依赖于createdAt + const startDate = ctx.validateQuery('start_date').optional().toString().val() + const endDate = ctx.validateQuery('end_date').optional().toString().val() + // 排序仅后台能用,且order和sortBy需同时传入才起作用 + // -1 desc | 1 asc + const order = ctx.validateQuery('order').optional().toInt().isIn([-1, 1], '排序方式参数无效').val() + // createdAt | updatedAt | ups + const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn(['createdAt', 'updatedAt', 'ups'], '排序项参数无效').val() + + // 过滤条件 + const options = { + sort: { createdAt: 1 }, + page, + limit: pageSize, + select: '', + populate: [ + { + path: 'author', + select: !ctx._isAuthenticated ? 'github' : '' + }, + { + path: 'parent', + select: 'author meta sticky ups', + match: { + state: 1 + } + }, + { + path: 'forward', + select: 'author meta sticky ups', + match: { + state: 1 + } + } + ] + } + + // 查询条件 + const query = {} + + if (type !== undefined) { + query.type = type + } + + if (state !== undefined) { + query.state = state + } + + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { content: keywordReg } + ] + } + + // 用户 + if (author) { + // 如果是id + if (isObjectId(author)) { + query.author = author + } else { + // 普通字符串,需要先查到id + const u = await UserModel.findOne({ name: author }).exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + query.author = u ? u._id : createObjectId() + } + } + + // 文章 + if (article) { + // 如果是id + if (isObjectId(article)) { + query.article = article + } else { + // 普通字符串,需要先查到id + const a = await ArticleModel.findOne({ name: article }).exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + query.article = a ? a._id : createObjectId() + } + } + + // 未通过权限校验(前台获取评论列表) + if (!ctx._isAuthenticated) { + // 将评论状态重置为1 + query.state = 1 + query.akimetSpam = false + // 评论列表不需要content和state + options.select = '-content -state -updatedAt -akimetSpam -type' + } else { + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } + + // 起始日期 + if (startDate) { + const $gte = new Date(startDate) + if ($gte.toString() !== 'Invalid Date') { + query.createdAt = { $gte } + } + } + + // 结束日期 + if (endDate) { + const $lte = new Date(endDate) + if ($lte.toString() !== 'Invalid Date') { + query.createdAt = Object.assign({}, query.createdAt, { $lte }) + } + } + } + + const comments = await CommentModel.paginate(query, options).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (comments) { + ctx.success({ + list: comments.docs, + pagination: { + total: comments.total, + current_page: comments.page > comments.pages ? comments.pages : comments.page, + total_page: comments.pages, + per_page: comments.limit + } + }) + } else { + ctx.fail(-1) + } +} + +exports.item = async (ctx, next) => { + const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + + let data = null + let queryPs = null + if (!ctx._isAuthenticated) { + queryPs = CommentModel.findById(id, { state: 1, akimetSpam: false }) + .select('-content -state -updatedAt -type -akimetSpam') + .populate({ + path: 'author', + select: 'github' + }) + .populate({ + path: 'parent', + select: 'author meta sticky ups' + }) + .populate({ + path: 'forward', + select: 'author meta sticky ups' + }) + } else { + queryPs = CommentModel.findById(id) + } + + data = await queryPs.exec().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + data = data.toObject() + ctx.success(data) + } else { + ctx.fail('评论不存在') + } +} + +exports.create = async (ctx, next) => { + const content = ctx.validateBody('content') + .required('内容参数必填') + .notEmpty() + .isString('内容参数必须是字符串类型') + .val() + const author = ctx.validateBody('author').required('用户ID参数必填').toString().isObjectId('用户ID参数无效').val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() + const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], '置顶参数无效').val() + const type = ctx.validateBody('type').defaultTo(0).toInt().isIn([0, 1], '评论类型参数无效').val() + const article = ctx.validateBody('article').optional().toString().isObjectId('文章ID参数无效').val() + const parent = ctx.validateBody('parent').optional().toString().isObjectId('父评论ID参数无效').val() + const forward = ctx.validateBody('forward').optional().toString().isObjectId('前置评论ID参数无效').val() + const req = ctx.req + const comment = { + content, + renderedContent: marked(content) + } + + if (type === undefined || type === 0) { + if (!article) { + return ctx.fail('缺少文章ID参数') + } + comment.article = article + } + + if (parent && !forward || !parent && forward) { + return ctx.fail('父评论ID和前置评论ID必须同时存在') + } + + author && (comment.author = author) + parent && (comment.parent = parent) + forward && (comment.forward = forward) + + if (state !== undefined) { + comment.state = state + } + + if (type !== undefined) { + comment.type = type + } + + if (sticky !== undefined) { + comment.sticky = sticky + } + + // 获取ip + const ip = (req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + req.connection.socket.remoteAddress || + req.ip || + req.ips[0]).replace('::ffff:', '') + const location = geoip.lookup(ip) + comment.meta = {} + comment.meta.location = location || null + comment.meta.ip = ip + comment.meta.ua = req.headers['user-agent'] || '' + comment.meta.referer = req.headers.referer || '' + + let data = await new CommentModel(comment).save().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + let p = CommentModel.findById(data._id) + if (!ctx._isAuthenticated) { + p = p.select('-content -state -updatedAt') + .populate({ + path: 'author', + select: 'github' + }) + .populate({ + path: 'parent', + select: 'author meta sticky ups' + }) + .populate({ + path: 'forward', + select: 'author meta sticky ups' + }) + } + data = await p + .exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.update = async (ctx, next) => { + const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + const content = ctx.validateBody('content').optional().isString('内容参数必须是字符串类型').val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() + const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], '置顶参数无效').val() + const akimetSpam = ctx.validateBody('akimet_spam').optional().toBoolean().val() + const comment = {} + let cache = await CommentModel.findById(id) + .populate('author') + .exec() + if (!cache) { + return ctx.fail('评论不存在') + } + cache = cache.toObject() + if (ctx._isAuthenticated && ctx._user._id.toString() !== cache.author._id.toString()) { + return ctx.fail('其他人的评论内容不能修改') + } + + if (content !== undefined) { + comment.content = content + comment.renderedContent = marked(content) + } + + if (state !== undefined) { + comment.state = state + } + + if (sticky !== undefined) { + comment.sticky = sticky + } + + if (akimetSpam !== undefined) { + comment.akimetSpam = akimetSpam + } + + let p = CommentModel.findByIdAndUpdate(id, comment, { new: true }) + if (!ctx._isAuthenticated) { + p = p.select('-content -state -updatedAt') + .populate({ + path: 'author', + select: 'github' + }) + .populate({ + path: 'parent', + select: 'author meta sticky ups' + }) + .populate({ + path: 'forward', + select: 'author meta sticky ups' + }) + } + const data = await p.exec().catch(err => { + ctx.log.error(err.message) + return null + }) + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.delete = async (ctx, next) => { + const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + const data = await CommentModel.remove({ _id: id }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data && data.result && data.result.ok) { + ctx.success() + } else { + ctx.fail('评论不存在') + } +} + +exports.like = async (ctx, next) => { + const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + + const data = await CommentModel.findByIdAndUpdate(id, { + $inc: { + ups: 1 + } + }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success() + } else { + ctx.fail('评论不存在') + } +} diff --git a/server/controller/index.js b/server/controller/index.js index 1d358fd..fe92693 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -9,6 +9,7 @@ exports.article = require('./article') exports.category = require('./category') exports.tag = require('./tag') +exports.comment = require('./comment') exports.music = require('./music') exports.option = require('./option') exports.user = require('./user') diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index 3e7501b..87b19c8 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -56,7 +56,7 @@ exports.isAuthenticated = () => { if (!user) { return ctx.fail(401, '用户不存在') } - ctx._user = user + ctx._user = user.toObject() ctx._isAuthenticated = true await next() } diff --git a/server/middleware/response.js b/server/middleware/response.js index 6e7f7a3..6f4c7e1 100644 --- a/server/middleware/response.js +++ b/server/middleware/response.js @@ -7,6 +7,7 @@ 'use strict' const config = require('../config') +const { isType } = require('../util') module.exports = async (ctx, next) => { ctx.success = (data = null, message = config.codeMap[200]) => { @@ -20,6 +21,11 @@ module.exports = async (ctx, next) => { } ctx.fail = (code = -1, message = '', data = null) => { + if (isType(code, 'String')) { + data = message || null + message = code + code = -1 + } ctx.status = 200 ctx.body = { code, diff --git a/server/model/schema/comment.js b/server/model/schema/comment.js index 1b46242..e936560 100644 --- a/server/model/schema/comment.js +++ b/server/model/schema/comment.js @@ -17,30 +17,21 @@ const commentSchema = new mongoose.Schema({ renderedContent: { type: String, required: true, validate: /\S+/ }, // marked渲染后的内容 state: { type: Number, default: 1 }, // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 akimetSpam: { type: Boolean, default: false }, // Akismet判定是否是垃圾评论,方便后台check - author: { // 评论发布者 - name: { type: String, required: true, validate: /\S+/ }, // 姓名 - // 邮箱 - email: { type: String, required: true, validate: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/ }, - // 个人站点地址 - site: { type: String, validate: /^((https|http):\/\/)+[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/ } - }, + author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, ups: { type: Number, default: 0 }, // 点赞数 - sticky: { type: Boolean, default: false }, // 是否置顶 - type: { type: Number, default: 0 }, // 类型 0 文章评论 | 1 页面评论 包括留言板或者作品展示页面等 + sticky: { type: Number, default: 0 }, // 是否置顶 0 否 | 1 是 + type: { type: Number, default: 0 }, // 类型 0 文章评论 | 1 其他(保留) meta: { ip: String, // 用户IP location: Object, // IP所在地 - agent: { type: String, validate: /\S+/ }, // user agent + ua: { type: String, validate: /\S+/ }, // user agent referer: { type: String, default: '' } } , - extends: [{ - key: { type: String, validate: /\S+/ }, - value: { type: String, validate: /\S+/ } - }], - pageId: String, // 页面id,type = 0时是文章的ID,type = 1时是页面的name,option model中Menu的name + // type为0时此项存在 + article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, // 子评论具备项 parent: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, // 父评论 parent和forward二者必须同时存在 - forward: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' } // 前一条评论ID,可以是parent_id, 比如 B评论 是 A评论的回复,则B.forward_id = A._id,主要是为了查看评论对话时的评论树构建 + forward: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' } // 前一条评论ID,可以是parent的id, 比如 B评论 是 A评论的回复,则B.forward._id = A._id,主要是为了查看评论对话时的评论树构建 }) commentSchema.plugin(mongoosePaginate) diff --git a/server/model/schema/option.js b/server/model/schema/option.js index c65929f..ac03885 100644 --- a/server/model/schema/option.js +++ b/server/model/schema/option.js @@ -41,7 +41,7 @@ const optionSchema = new mongoose.Schema({ slogan: { type: String, default: '' }, site: { type: String, required: true } }], - musicId: { type: String, default: '' } + musicId: { type: String, default: '' }, }) module.exports = optionSchema diff --git a/server/routes/backend.js b/server/routes/backend.js index 5c77207..927fa78 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -7,31 +7,39 @@ 'use strict' const router = require('koa-router')() -const { article, category, tag, option, user, auth, music, statistics } = require('../controller') +const { article, category, tag, comment, option, user, auth, music, statistics } = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() // Article router.get('/articles', isAuthenticated, article.list) router.get('/articles/:id', isAuthenticated, article.item) -router.post('/articles', article.create) +router.post('/articles',isAuthenticated, article.create) router.patch('/articles/:id', isAuthenticated, article.update) router.delete('/articles/:id', isAuthenticated, article.delete) router.post('/articles/:id/like', isAuthenticated, article.like) +// Comment +router.get('/comments', isAuthenticated, comment.list) +router.get('/comments/:id', isAuthenticated, comment.item) +router.post('/comments', isAuthenticated, comment.create) +router.patch('/comments/:id', isAuthenticated, comment.update) +router.delete('/comments/:id', isAuthenticated, comment.delete) +router.post('/comments/:id/like', isAuthenticated, comment.like) + // Tag router.get('/tags', isAuthenticated, tag.list) router.get('/tags/:id', isAuthenticated, tag.item) router.post('/tags', isAuthenticated, tag.create) router.patch('/tags/:id', isAuthenticated, tag.update) -router.delete('/tags/:id', tag.delete) +router.delete('/tags/:id', isAuthenticated, tag.delete) // Category router.get('/categories', isAuthenticated, category.list) router.get('/categories/:id', isAuthenticated, category.item) router.post('/categories', isAuthenticated, category.create) router.patch('/categories/:id', isAuthenticated, category.update) -router.delete('/categories/:id', category.delete) +router.delete('/categories/:id', isAuthenticated, category.delete) // Option router.get('/options', isAuthenticated, option.data) diff --git a/server/routes/frontend.js b/server/routes/frontend.js index f941804..ad85de1 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -7,7 +7,7 @@ 'use strict' const router = require('koa-router')() -const { article, category, tag, music, option, user, auth } = require('../controller') +const { article, category, tag, comment, music, option, user, auth } = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() const snsAuth = authenticate.snsAuth @@ -18,6 +18,12 @@ router.get('/articles', article.list) router.get('/articles/:id', article.item) router.post('/articles/:id/like', article.like) +// Comment +router.get('/comments', comment.list) +router.get('/comments/:id', comment.item) +router.post('/comments', comment.create) +router.post('/comments/:id/like', comment.like) + // Tag router.get('/tags', tag.list) router.get('/tags/:id', tag.item) From 596f7a3dfb32d72a33d09726680aa87c208b14db Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 29 Oct 2017 01:28:59 +0800 Subject: [PATCH 048/208] [feature] add Akismet Client support --- README.md | 8 +- server/akismet.js | 147 +++++++++++++++++++++++++++++++++++ server/app.js | 5 +- server/config/development.js | 3 + server/config/index.js | 5 +- server/util/debug.js | 33 +++++--- 6 files changed, 182 insertions(+), 19 deletions(-) create mode 100644 server/akismet.js diff --git a/README.md b/README.md index 47308a0..a420050 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ My blog's api server build by koa2 and mongoose * ~~Redis缓存部分数据~~ (2017.10.27 v1.1) -* 垃圾评论过滤 [akismet](https://github.com/chrisfosterelli/akismet-api) +* ~~评论api~~ (2017.10.29) -* 评论定位 [geoip](https://github.com/bluesmoon/node-geoip) +* ~~评论定位 [geoip](https://github.com/bluesmoon/node-geoip)~~ (2017.10.29) -* 评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer) +* ~~垃圾评论过滤 [akismet](https://github.com/chrisfosterelli/akismet-api)~~ (2017.10.30) -* 评论api +* 评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer) * 消息api diff --git a/server/akismet.js b/server/akismet.js new file mode 100644 index 0000000..359c6b4 --- /dev/null +++ b/server/akismet.js @@ -0,0 +1,147 @@ +/** + * @desc Akismet + * @author Jooger + * @date 29 Oct 2017 + */ + +'use strict' + +const akismet = require('akismet-api') +const config = require('./config') +const { isType, getDebug } = require('./util') +const debug = getDebug('Akismet') +let akismetClient = null + +// Akismet apikey是否验证通过 +let isValidKey = false + +/** + * @desc Akismet Client Class + * @param {String} [required] key Akismet apikey + * @param {String} [required] site Akismet site + */ +class AkismetClient { + constructor (key, site) { + this.key = key + this.site = site + this.initClient() + } + + initClient () { + this.client = akismet.client({ + key: this.key, + blog: this.site + }) + } + + async verifyKey () { + let valid = true + if (!isValidKey) { + await this.client.verifyKey().then(v => { + valid = v + if (v) { + isValidKey = true + } else { + debug.error(`无效的Apikey`) + this.client = null + } + }).catch(err => debug.error('Apikey验证失败,错误:', err.message)) + } + return { valid, client: this } + } + + // 检测是否是spam + checkSpam (opt = {}) { + debug.info('验证评论中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.checkSpam(opt, (err, spam) => { + if (err) { + debug.error('评论验证失败,将跳过Spam验证,错误:', err.message) + return reject(false) + } + if (spam) { + debug.warn('评论验证不通过,疑似垃圾评论') + resolve(true) + } else { + debug.success('评论验证通过') + resolve(false) + } + }) + } else { + debug.warn('Apikey未认证,将跳过Spam验证') + resolve(false) + } + }) + } + + // 提交被误检为spam的正常评论 + submitSpam (opt = {}) { + debug.info('误检Spam垃圾评论报告提交中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.submitSpam(opt, err => { + if (err) { + debug.error('误检Spam垃圾评论报告提交失败') + return reject(err) + } + debug.success('误检Spam垃圾评论报告提交成功') + resolve() + }) + } else { + debug.warn('Apikey未认证,误检Spam垃圾评论报告提交失败') + resolve() + } + }) + } + + // 提交被误检为正常评论的spam + submitHam (opt = {}) { + debug.info('误检正常评论报告提交中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.submitSpam(opt, err => { + if (err) { + debug.error('误检正常评论报告提交失败') + return reject(err) + } + debug.success('误检正常评论报告提交成功') + resolve() + }) + } else { + debug.warn('Apikey未认证,误检正常评论报告提交失败') + resolve() + } + }) + } +} + +/** + * @desc 生成Akismet clients + */ +exports.start = async () => { + const akismetConfig = config.akismet + const { apiKey } = akismetConfig + const site = config.site + const { valid, client } = await new AkismetClient(apiKey, site).verifyKey() + + if (valid) { + debug.success('服务启动成功') + akismetClient = client + } else { + debug.error('服务启动失败') + } +} + +/** + * @desc 根据站点地址获取对应Akismet client + * @param {String} site 站点地址 + * @return {AkismetClient} akismetClient Akismet client + */ +exports.getAkismetClient = () => { + if (!akismetClient) { + debug.warn('未找到客户端,将跳过spam验证') + return null + } + return akismetClient +} diff --git a/server/app.js b/server/app.js index 8f5c45b..87ad100 100644 --- a/server/app.js +++ b/server/app.js @@ -55,7 +55,10 @@ app.use(compress()) // routes require('./routes')(app) -// +// crontab require('./service/crontab').start() +// akismet +require('./akismet').start() + module.exports = app diff --git a/server/config/development.js b/server/config/development.js index b0fe2a7..0c3f6ba 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -18,5 +18,8 @@ module.exports = { clientSecret: '8771bd9ae52749cc15b0c9e2c6cb4ecd7f39d9da', callbackURL: 'http://127.0.0.1:3001/auth/github/login/callback' } + }, + akismet: { + apiKey: '7fa12f4a1d08' } } diff --git a/server/config/index.js b/server/config/index.js index e995f1e..e79f322 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -69,10 +69,7 @@ const baseConfig = { } }, akismet: { - apiKey: 'akismet api key', - activeSites: [ - packageInfo.site - ] + apiKey: 'akismet api key' } } diff --git a/server/util/debug.js b/server/util/debug.js index b6655c0..077376d 100644 --- a/server/util/debug.js +++ b/server/util/debug.js @@ -10,25 +10,38 @@ const debug = require('debug') const packageInfo = require('../../package.json') const levelMap = { - success: 2, - info: 6, - warn: 3, - error: 1 + success: { + level: 2, + emoji: '✅' + }, + info: { + level: 6, + emoji: '🤗' + }, + warn: { + level: 3, + emoji: '⚠️' + }, + error: { + level: 1, + emoji: '❌' + } } const slice = Array.prototype.slice module.exports = function getDebug (namespace = '') { - const deBug = debug(`[${packageInfo.name}]${namespace ? ' ' + namespace : ''}`) - + const deBug = debug(`[${packageInfo.name}] ${namespace || ''}`) function d () { d.info.apply(d, slice.call(arguments)) } - Object.keys(levelMap).map(level => { - d[level] = function () { + Object.keys(levelMap).map(key => { + d[key] = function () { deBug.enabled = true - deBug.color = levelMap[level] - deBug.apply(null, slice.call(arguments)) + deBug.color = levelMap[key].level + const args = slice.call(arguments) + args[0] = levelMap[key].emoji + ' ' + args[0] + deBug.apply(null, args) } }) From 3432f5157367676cd4a6406ea72dafa3899e3032 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 29 Oct 2017 13:57:04 +0800 Subject: [PATCH 049/208] [update] update admin seed way, change to github user fetching --- server/config/index.js | 2 +- server/controller/user.js | 2 +- server/mongo.js | 59 +++++++++++++++++++++++++++++---------- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index e79f322..b895ca8 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -51,7 +51,7 @@ const baseConfig = { }, userCookieKey: 'jooger.me.userid', secrets: `${packageInfo.name} ${packageInfo.version}`, - defaultName: 'Jooger', + defaultName: 'jo0ger', defaultPassword: 'admin_jooger', // 允许请求的域名 allowedOrigins: [ diff --git a/server/controller/user.js b/server/controller/user.js index d6c8865..df638c8 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -37,7 +37,7 @@ exports.item = async (ctx, next) => { let select = '-password' if (!ctx._isAuthenticated) { - select += ' -role' + select += ' -role -createdAt -updatedAt' } const data = await UserModel.findById(id) diff --git a/server/mongo.js b/server/mongo.js index d054476..81df8eb 100644 --- a/server/mongo.js +++ b/server/mongo.js @@ -6,10 +6,11 @@ 'use strict' -const mongoose = require('mongoose') const config = require('./config') -const { UserModel, OptionModel } = require('./model') +const mongoose = require('mongoose') const { bhash, getDebug } = require('./util') +const { UserModel, OptionModel } = require('./model') +const { getGithubUsersInfo } = require('./service') const debug = getDebug('MongoDB') let isConnected = false @@ -45,19 +46,47 @@ async function seedOption () { } // 管理员初始化 -function seedAdmin () { - UserModel.findOne({ role: 0 }).exec().then(data => { - if (!data) { - createAdmin() +async function seedAdmin () { + const admin = await UserModel.findOne({ role: 0 }).exec() + .catch(err => debug.error('初始化管理员查询失败,错误:', err.message)) + if (!admin) { + createAdmin() + } +} + +async function createAdmin () { + let data = await getGithubUsersInfo(config.auth.defaultName) + + if (!data || !data[0]) { + return fail('未找到Github用户数据') + } + data = data[0] + const result = await new UserModel({ + role: 0, + name: data.name, + password: bhash(config.auth.defaultPassword), + slogan: data.bio, + avatar: data.avatar_url, + github: { + id: data.id, + email: data.email, + login: data.login, + name: data.name, + blog: data.blog } - }).catch(err => debug.error(err.message)) - - function createAdmin () { - new UserModel({ - role: 0, - password: bhash(config.auth.defaultPassword) - }) - .save() - .catch(err => debug.error(err.message)) + }) + .save() + .catch(err => { + fail(err.message) + }) + + if (!result || !result._id) { + fail('本地入库失败') + } else { + debug.success('初始化管理员成功') + } + + function fail (msg = '') { + debug.error('初始化管理员失败,错误:', msg) } } From 7a56d960e01c02b0e09ef112a5378402123611af Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 29 Oct 2017 17:43:24 +0800 Subject: [PATCH 050/208] [feature] creating comment supports spam check --- server/controller/comment.js | 97 ++++++++++++++++++++++++++++-------- server/model/schema/user.js | 2 + 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/server/controller/comment.js b/server/controller/comment.js index 37cc509..98b23b0 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -8,6 +8,7 @@ const geoip = require('geoip-lite') const config = require('../config') +const { getAkismetClient } = require('../akismet') const { CommentModel, UserModel, ArticleModel } = require('../model') const { marked, isObjectId, createObjectId, getDebug } = require('../util') const debug = getDebug('Comment') @@ -210,38 +211,30 @@ exports.create = async (ctx, next) => { const parent = ctx.validateBody('parent').optional().toString().isObjectId('父评论ID参数无效').val() const forward = ctx.validateBody('forward').optional().toString().isObjectId('前置评论ID参数无效').val() const req = ctx.req - const comment = { - content, - renderedContent: marked(content) + const comment = { content } + + const user = await UserModel.findById(author).select('github').exec().catch(err => { + debug.error('用户查找失败,错误:', err.message) + ctx.log.error(err.message) + return null + }) + + if (!user) { + return ctx.fail('用户不存在') } + if (type === undefined || type === 0) { if (!article) { return ctx.fail('缺少文章ID参数') } comment.article = article } - + if (parent && !forward || !parent && forward) { return ctx.fail('父评论ID和前置评论ID必须同时存在') } - author && (comment.author = author) - parent && (comment.parent = parent) - forward && (comment.forward = forward) - - if (state !== undefined) { - comment.state = state - } - - if (type !== undefined) { - comment.type = type - } - - if (sticky !== undefined) { - comment.sticky = sticky - } - // 获取ip const ip = (req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || @@ -257,6 +250,47 @@ exports.create = async (ctx, next) => { comment.meta.ua = req.headers['user-agent'] || '' comment.meta.referer = req.headers.referer || '' + // 先判断是不是垃圾邮件 + const akismetClient = getAkismetClient() + let isSpam = false + const permalink = getPermalink(comment) + if (akismetClient) { + isSpam = await akismetClient.checkSpam({ + user_ip : ip, // Required! + user_agent : comment.meta.ua, // Required! + referrer : comment.meta.referer, // Required! + permalink, + comment_type : getCommentType(type), + comment_author : user.github.login, + comment_author_email : user.github.email, + comment_author_url : user.github.blog, + comment_content : content, + is_test : process.env.NODE_ENV === 'development' + }) + } + + // 如果是Spam评论 + if (isSpam) { + return ctx.fail('检测为垃圾评论,该评论将不会显示') + } + + parent && (comment.parent = parent) + forward && (comment.forward = forward) + comment.renderedContent = marked(content) + comment.author = author + + if (state !== undefined) { + comment.state = state + } + + if (type !== undefined) { + comment.type = type + } + + if (sticky !== undefined) { + comment.sticky = sticky + } + let data = await new CommentModel(comment).save().catch(err => { ctx.log.error(err.message) return null @@ -385,3 +419,26 @@ exports.like = async (ctx, next) => { ctx.fail('评论不存在') } } + +function getPermalink (comment = {}) { + const { type, article } = comment + switch (type) { + case 0: + return `${config.site}/blog/article/${article}` + break + // TODO: 其他页面或组件的permalink + default: + break + } +} + +function getCommentType (type) { + switch (type) { + case 0: + return '博客文章评论' + break + default: + return '其他评论' + break + } +} diff --git a/server/model/schema/user.js b/server/model/schema/user.js index 92fa799..777c843 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -19,6 +19,8 @@ const userSchema = new mongoose.Schema({ avatar: { type: String, default: '' }, // 角色 0 管理员 | 1 普通用户 role: { type: Number, default: 1 }, + // 是否被禁言 + mute: { type: Boolean, default: false }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, github: { From ded7dffce30990bb8f276b79bd644ba70f68d555 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 29 Oct 2017 20:13:39 +0800 Subject: [PATCH 051/208] [feature] comment api supports mail sending --- README.md | 8 +- package.json | 6 +- server/app.js | 19 ++- server/config/index.js | 1 + server/controller/auth.js | 9 +- server/controller/comment.js | 210 +++++++++++++++++++++++------ server/controller/music.js | 2 +- server/controller/option.js | 5 +- server/controller/user.js | 50 ++++++- server/model/schema/comment.js | 2 +- server/{ => plugins}/akismet.js | 4 +- server/plugins/index.js | 13 ++ server/plugins/mailer.js | 61 +++++++++ server/{ => plugins}/mongo.js | 10 +- server/{ => plugins}/redis.js | 4 +- server/{ => plugins}/validation.js | 2 +- server/service/crontab.js | 6 +- server/service/github-passport.js | 6 +- server/util/index.js | 4 +- 19 files changed, 345 insertions(+), 77 deletions(-) rename server/{ => plugins}/akismet.js (97%) create mode 100644 server/plugins/index.js create mode 100644 server/plugins/mailer.js rename server/{ => plugins}/mongo.js (88%) rename server/{ => plugins}/redis.js (94%) rename server/{ => plugins}/validation.js (96%) diff --git a/README.md b/README.md index a420050..b111c7c 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,15 @@ My blog's api server build by koa2 and mongoose * ~~Redis缓存部分数据~~ (2017.10.27 v1.1) -* ~~评论api~~ (2017.10.29) +* ~~评论api~~ (2017.10.28) * ~~评论定位 [geoip](https://github.com/bluesmoon/node-geoip)~~ (2017.10.29) -* ~~垃圾评论过滤 [akismet](https://github.com/chrisfosterelli/akismet-api)~~ (2017.10.30) +* ~~垃圾评论过滤 [akismet](https://github.com/chrisfosterelli/akismet-api)~~ (2017.10.29) -* 评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer) +* ~~用户禁言~~ (2017.10.29) + +* ~~评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer)~~ (2017.10.29) * 消息api diff --git a/package.json b/package.json index 82e52cf..f526089 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "jooger.me-server", - "version": "1.1.0", + "version": "1.2.0", "private": true, + "description": "🔥 My blog's api server build by koa2 and mongoose", "scripts": { "dev": "cross-env NODE_ENV=development nodemon bin/www", "debug": "cross-env NODE_ENV=development nodemon --inspect bin/www", @@ -10,7 +11,9 @@ "test": "echo \"Error: no test specified\" && exit 1", "deploy": "pm2 deploy ecosystem.config.js production" }, + "author": "Jooger", "site": "https://jooger.me", + "email": "zzy1198258955@163.com", "repository": { "type": "https", "url": "https://github.com/jo0ger/jooger.me-server.git" @@ -23,7 +26,6 @@ "Koa2", "MongoDB" ], - "author": "Jooger", "license": "MIT", "bugs": { "url": "https://github.com/jo0ger/jooger.me-server/issues" diff --git a/server/app.js b/server/app.js index 87ad100..dbf2825 100644 --- a/server/app.js +++ b/server/app.js @@ -18,17 +18,19 @@ const bodyparser = require('koa-bodyparser') const koaBunyanLogger = require('koa-bunyan-logger') const middlewares = require('./middleware') const config = require('./config') +const { mongo, redis, akismet, validation, mailer } = require('./plugins') +const { crontab } = require('./service') const app = new Koa() // connect mongodb -require('./mongo').connect() +mongo.connect() // connect redis -require('./redis').connect() +redis.connect() // load custom validations -bouncer.Validator = require('./validation') +bouncer.Validator = validation // error handler onerror(app) @@ -55,10 +57,13 @@ app.use(compress()) // routes require('./routes')(app) -// crontab -require('./service/crontab').start() - // akismet -require('./akismet').start() +akismet.start() + +// mailer +mailer.start() + +// crontab +crontab.start() module.exports = app diff --git a/server/config/index.js b/server/config/index.js index b895ca8..d13ca6f 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -15,6 +15,7 @@ const baseConfig = { version: packageInfo.version, author: packageInfo.author || 'Jooger', site: packageInfo.site, + email: packageInfo.email, env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3001, diff --git a/server/controller/auth.js b/server/controller/auth.js index e75e15a..6a40b5b 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -11,7 +11,8 @@ const passport = require('koa-passport') const config = require('../config') const { UserModel } = require('../model') const { bhash, bcompare, getDebug, signToken } = require('../util') -const debug = getDebug('Github:Auth') +const debug = getDebug('Auth') +const debugGithub = getDebug('Github:Auth') const { githubPassport } = require('../service') githubPassport.init(UserModel, config) @@ -96,7 +97,7 @@ exports.githubLogin = async (ctx, next) => { await passport.authenticate('github', { session: false }, (err, user) => { - debug('Github权限验证回调处理开始') + debugGithub('Github权限验证回调处理开始') const redirectUrl = ctx.session.passport.redirectUrl const cookieDomain = config.auth.session.domain || null @@ -108,8 +109,8 @@ exports.githubLogin = async (ctx, next) => { ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) ctx.cookies.set(config.auth.userCookieKey, user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - debug('Github权限验证回调处理成功') - debug.success('Github权限验证回调处理成功, 用户ID:%s,用户名:%s', user._id, user.name) + debugGithub('Github权限验证回调处理成功') + debugGithub.success('Github权限验证回调处理成功, 用户ID:%s,用户名:%s', user._id, user.name) return ctx.redirect(redirectUrl) })(ctx) } diff --git a/server/controller/comment.js b/server/controller/comment.js index 98b23b0..a08fe02 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -8,10 +8,11 @@ const geoip = require('geoip-lite') const config = require('../config') -const { getAkismetClient } = require('../akismet') +const { akismet, mailer } = require('../plugins') const { CommentModel, UserModel, ArticleModel } = require('../model') const { marked, isObjectId, createObjectId, getDebug } = require('../util') const debug = getDebug('Comment') +const isProd = process.env.NODE_ENV === 'development' exports.list = async (ctx, next) => { const pageSize = ctx.validateQuery('per_page').defaultTo(config.commentLimit).toInt().gt(0, '每页评论数量必须大于0').val() @@ -113,9 +114,9 @@ exports.list = async (ctx, next) => { if (!ctx._isAuthenticated) { // 将评论状态重置为1 query.state = 1 - query.akimetSpam = false + query.spam = false // 评论列表不需要content和state - options.select = '-content -state -updatedAt -akimetSpam -type' + options.select = '-content -state -updatedAt -spam -type' } else { // 排序 if (sortBy && order) { @@ -166,8 +167,8 @@ exports.item = async (ctx, next) => { let data = null let queryPs = null if (!ctx._isAuthenticated) { - queryPs = CommentModel.findById(id, { state: 1, akimetSpam: false }) - .select('-content -state -updatedAt -type -akimetSpam') + queryPs = CommentModel.findById(id, { state: 1, spam: false }) + .select('-content -state -updatedAt -type -spam') .populate({ path: 'author', select: 'github' @@ -213,6 +214,17 @@ exports.create = async (ctx, next) => { const req = ctx.req const comment = { content } + if (type === undefined || type === 0) { + if (!article) { + return ctx.fail('缺少文章ID参数') + } + comment.article = article + } + + if (parent && !forward || !parent && forward) { + return ctx.fail('父评论ID和前置评论ID必须同时存在') + } + const user = await UserModel.findById(author).select('github').exec().catch(err => { debug.error('用户查找失败,错误:', err.message) ctx.log.error(err.message) @@ -221,18 +233,25 @@ exports.create = async (ctx, next) => { if (!user) { return ctx.fail('用户不存在') + } else if (user.mute) { + // 如果被禁言 + return ctx.fail('您已经被禁言') } - - if (type === undefined || type === 0) { - if (!article) { - return ctx.fail('缺少文章ID参数') - } - comment.article = article + if (!checkUserSpam(user)) { + return ctx.fail('您的垃圾评论数量已达到最大限制,已被禁言') } - - if (parent && !forward || !parent && forward) { - return ctx.fail('父评论ID和前置评论ID必须同时存在') + + if (state !== undefined) { + comment.state = state + } + + if (type !== undefined) { + comment.type = type + } + + if (sticky !== undefined) { + comment.sticky = sticky } // 获取ip @@ -251,8 +270,9 @@ exports.create = async (ctx, next) => { comment.meta.referer = req.headers.referer || '' // 先判断是不是垃圾邮件 - const akismetClient = getAkismetClient() + const akismetClient = akismet.getAkismetClient() let isSpam = false + // 永链 const permalink = getPermalink(comment) if (akismetClient) { isSpam = await akismetClient.checkSpam({ @@ -265,7 +285,7 @@ exports.create = async (ctx, next) => { comment_author_email : user.github.email, comment_author_url : user.github.blog, comment_content : content, - is_test : process.env.NODE_ENV === 'development' + is_test : isProd }) } @@ -279,18 +299,6 @@ exports.create = async (ctx, next) => { comment.renderedContent = marked(content) comment.author = author - if (state !== undefined) { - comment.state = state - } - - if (type !== undefined) { - comment.type = type - } - - if (sticky !== undefined) { - comment.sticky = sticky - } - let data = await new CommentModel(comment).save().catch(err => { ctx.log.error(err.message) return null @@ -302,7 +310,7 @@ exports.create = async (ctx, next) => { p = p.select('-content -state -updatedAt') .populate({ path: 'author', - select: 'github' + select: 'name github' }) .populate({ path: 'parent', @@ -320,6 +328,11 @@ exports.create = async (ctx, next) => { return null }) ctx.success(data) + // 如果是文章评论,则更新文章评论数量 + if (type === 0) { + updateArticleCommentCount([comment.article]) + } + sendEmailToAdminAndUser(data, permalink) } else { ctx.fail() } @@ -328,9 +341,8 @@ exports.create = async (ctx, next) => { exports.update = async (ctx, next) => { const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() const content = ctx.validateBody('content').optional().isString('内容参数必须是字符串类型').val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() + const state = ctx.validateBody('state').optional().toInt().isIn([-2, 0, 1, 2], '评论状态参数无效').val() const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], '置顶参数无效').val() - const akimetSpam = ctx.validateBody('akimet_spam').optional().toBoolean().val() const comment = {} let cache = await CommentModel.findById(id) .populate('author') @@ -348,16 +360,43 @@ exports.update = async (ctx, next) => { comment.renderedContent = marked(content) } - if (state !== undefined) { - comment.state = state - } - if (sticky !== undefined) { comment.sticky = sticky } - - if (akimetSpam !== undefined) { - comment.akimetSpam = akimetSpam + + // 状态修改是涉及到spam修改 + if (state !== undefined) { + comment.state = state + const akismetClient = akismet.getAkismetClient() + const permalink = getPermalink(cache) + const opt = { + user_ip : cache.meta.ip, // Required! + user_agent : cache.meta.ua, // Required! + referrer : cache.meta.referer, // Required! + permalink, + comment_type : getCommentType(cache.type), + comment_author : cache.author.github.login, + comment_author_email : cache.author.github.email, + comment_author_url : cache.author.github.blog, + comment_content : cache.content, + is_test : isProd + } + + if (cache.state === -2 && state !== -2) { + // 垃圾评论转为正常评论 + if (cache.spam) { + comment.spam = false + // 报告给Akismet + akismetClient.submitSpam(opt) + } + } else if (cache.state !== -2 && state === -2) { + // 正常评论转为垃圾评论 + if (!cache.spam) { + comment.spam = true + // 报告给Akismet + akismetClient.submitHam(opt) + } + } } let p = CommentModel.findByIdAndUpdate(id, comment, { new: true }) @@ -420,6 +459,7 @@ exports.like = async (ctx, next) => { } } +// 获取永久链接 function getPermalink (comment = {}) { const { type, article } = comment switch (type) { @@ -428,10 +468,12 @@ function getPermalink (comment = {}) { break // TODO: 其他页面或组件的permalink default: + return '' break } } +// 评论类型说明 function getCommentType (type) { switch (type) { case 0: @@ -442,3 +484,95 @@ function getCommentType (type) { break } } + +// 检测用户以往spam评论 +async function checkUserSpam (user) { + const userComments = await CommentModel.find({ + author: user._id + }) + .exec() + .catch(err => { + debug.error('用户历史评论获取失败,错误:', err.message) + return [] + }) + + const spamComments = userComments.filter(c => c.spam) + // 如果用户以往评论中spam评论数量大于等于spam限制 + if (spamComments.length >= config.commentSpamLimit) { + if (!user.mute) { + // 将用户禁言 + await UserModel.update({ _id: user._id }, { + mute: true + }) + .exec() + .then(() => { + debug.success('用户禁言成功,用户:', user.name) + }) + .catch(err => { + debug.error('用户禁言失败,请手动禁言,错误:', err.message) + }) + } + return false + } + return true +} + +// 更新文章的meta.comments评论数量 +async function updateArticleCommentCount (articleIds = []) { + if (!articleIds.length) { + return + } + // TIP: 这里必须$in的是一个ObjectId对象数组,而不能只是id字符串数组 + articleIds = [...new Set(articleIds)].filter(id => isObjectId(id)).map(id => createObjectId(id)) + const counts = await CommentModel.aggregate([ + { $match: { state: 1, article: { $in: articleIds } } }, + { $group: { _id: '$article', total_count: { $sum: 1 } } } + ]) + .exec() + .catch(err => { + debug.error('更新文章评论数量前聚合评论数据操作失败,错误:', err.message) + return [] + }) + Promise.all(counts.map(count => ArticleModel.update( + { _id: count._id }, + { $set: { 'meta.comments': count.total_count } } + ).exec().catch(err => { + debug.error('文章评论数量更新失败,错误:', err.message) + }))).then(() => { + debug.success('文章评论数量更新成功') + }) +} + +// 发送邮件 +async function sendEmailToAdminAndUser (comment, permalink) { + const { type, article } = comment + let adminTitle = '博客有新的留言' + if (type == 0) { + // 文章评论 + const at = await ArticleModel.findById(article).exec() + if (at && at._id) { + adminTitle = `博客文章 [${at.title}] 有了新的评论` + } + } + // 发送给管理员邮箱config.email + mailer.send({ + subject: adminTitle, + text: `来自 ${comment.author.github.name} 的${type == 0 ? '评论' : '留言'}:${comment.content}`, + html: `

来自 ${comment.author.github.name} 的${type == 0 ? '评论' : '留言'} [ 点击查看 ]:${comment.renderedContent}

` + }, true) + + // 发送给被评论者 + if (comment.forward) { + const forwardAuthor = await UserModel.findById(comment.forward.author).exec().catch(err => null) + if (forwardAuthor) { + mailer.send({ + to: forwardAuthor.github.email, + subject: '你在Jooger的博客的评论有了新的回复', + text: `来自 ${comment.author.name} 的回复:${comment.content}`, + html: `

来自 ${comment.author.name} 的回复 [ 点击查看 ]:${comment.renderedContent}

` + }) + } else { + debug.warn('给被评论者邮件失败') + } + } +} diff --git a/server/controller/music.js b/server/controller/music.js index 0874638..98a91f5 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -11,7 +11,7 @@ const config = require('../config') const { fetchNE } = require('../service') const { OptionModel } = require('../model') const { proxy, getDebug } = require('../util') -const redis = require('../redis') +const { redis } = require('../plugins') const isProd = process.env.NODE_ENV === 'production' const debug = getDebug('Music') diff --git a/server/controller/option.js b/server/controller/option.js index b16c70e..64a4692 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -9,7 +9,8 @@ const { OptionModel } = require('../model') const { getGithubUsersInfo } = require('../service') const { updateMusicCache } = require('./music') -const debug = require('../util').getDebug('Option') +const { getDebug, proxy } = require('../util') +const debug = getDebug('Option') exports.data = async (ctx, next) => { const data = await OptionModel.findOne().exec().catch(err => { @@ -79,7 +80,7 @@ async function generateLinks (links = []) { return links.map((link, index) => { const userInfo = usersInfo[index] if (userInfo) { - link.avatar = userInfo.avatar_url + link.avatar = proxy(userInfo.avatar_url) link.slogan = userInfo.bio link.site = link.site || userInfo.blog } diff --git a/server/controller/user.js b/server/controller/user.js index df638c8..093744b 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -6,14 +6,16 @@ 'use strict' -const { UserModel } = require('../model') -const { bhash, bcompare } = require('../util') const config = require('../config') +const { UserModel } = require('../model') +const { bhash, bcompare, getDebug } = require('../util') +const { getGithubUsersInfo } = require('../service') +const debug = getDebug('User') exports.list = async (ctx, next) => { let select = '-password' if (!ctx._isAuthenticated) { - select += ' -createdAt -updatedAt' + select += ' -createdAt -updatedAt -role' } const data = await UserModel.find({}) @@ -62,7 +64,7 @@ exports.update = async (ctx, next) => { const description = ctx.validateBody('description').optional().isString('the "description" parameter should be String type').val() const avatar = ctx.validateBody('avatar').optional().isString('the "avatar" parameter should be String type').val() const role = ctx.validateBody('role').optional().toInt().isIn([0, 1], 'the "role" parameter is not the expected value').val() - + const mute = ctx.validateBody('mute').optional().toBoolean().val() const user = {} name && (user.name = name) @@ -74,6 +76,10 @@ exports.update = async (ctx, next) => { user.role = role } + if (mute !== undefined) { + user.mute = mute + } + if (password !== undefined) { const oldPassword = ctx.validateBody('old_password') .required('the "old_password" parameter is required') @@ -132,3 +138,39 @@ exports.me = async (ctx, next) => { ctx.fail() } } + +// 更新用户的Github信息 +exports.updateGithubInfo = async () => { + const users = await UserModel.find({}) + .exec() + .catch(err => { + debug.error('用户查找失败,错误:', err.message) + return [] + }) + const updates = await getGithubUsersInfo(users.map(user => user.github.login)) + Promise.all( + updates.map((data, index) => { + const user = users[index] + const u = { + github: { + id: data.id, + email: data.email, + login: data.login, + name: data.name, + blog: data.blog + } + } + // 非管理员更新其他信息,管理员只更新github信息 + if (user.role !== 0) { + u.name = data.name + u.slogan = data.bio + u.avatar = proxy(data.avatar_url) + } + return UserModel.findByIdAndUpdate(user._id, u).exec().catch(err => { + debug.error('用户Github信息更新失败,错误:', err.message) + }) + }) + ).then(() => { + debug.success('全部用户Github信息更新成功') + }) +} diff --git a/server/model/schema/comment.js b/server/model/schema/comment.js index e936560..ecb2eb8 100644 --- a/server/model/schema/comment.js +++ b/server/model/schema/comment.js @@ -16,7 +16,7 @@ const commentSchema = new mongoose.Schema({ content: { type: String, required: true, validate: /\S+/ }, // 评论内容 renderedContent: { type: String, required: true, validate: /\S+/ }, // marked渲染后的内容 state: { type: Number, default: 1 }, // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 - akimetSpam: { type: Boolean, default: false }, // Akismet判定是否是垃圾评论,方便后台check + spam: { type: Boolean, default: false }, // Akismet判定是否是垃圾评论,方便后台check author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, ups: { type: Number, default: 0 }, // 点赞数 sticky: { type: Number, default: 0 }, // 是否置顶 0 否 | 1 是 diff --git a/server/akismet.js b/server/plugins/akismet.js similarity index 97% rename from server/akismet.js rename to server/plugins/akismet.js index 359c6b4..e325ee4 100644 --- a/server/akismet.js +++ b/server/plugins/akismet.js @@ -7,8 +7,8 @@ 'use strict' const akismet = require('akismet-api') -const config = require('./config') -const { isType, getDebug } = require('./util') +const config = require('../config') +const { isType, getDebug } = require('../util') const debug = getDebug('Akismet') let akismetClient = null diff --git a/server/plugins/index.js b/server/plugins/index.js new file mode 100644 index 0000000..f61aec9 --- /dev/null +++ b/server/plugins/index.js @@ -0,0 +1,13 @@ +/** + * @desc Plugins entry + * @author Jooger + * @date 29 Oct 2017 + */ + +'use strict' + +exports.mongo = require('./mongo') +exports.redis = require('./redis') +exports.akismet = require('./akismet') +exports.validation = require('./validation') +exports.mailer = require('./mailer') diff --git a/server/plugins/mailer.js b/server/plugins/mailer.js new file mode 100644 index 0000000..02b6e5c --- /dev/null +++ b/server/plugins/mailer.js @@ -0,0 +1,61 @@ +/** + * @desc Mail plugin + * @author Jooger + * @date 29 Oct 2017 + */ + +'use strict' + +const nodemailer = require('nodemailer') +const config = require('../config') +const { getDebug } = require('../util') +const debug = getDebug('Mailer') + +let isVerify = false +const transporter = nodemailer.createTransport({ + service: '163', + secure: true, + auth: { + user: config.email, + pass: '19950102zzy' + } +}) + +exports.start = async () => { + return new Promise((resolve, reject) => { + transporter.verify((err, success) => { + if (err) { + isVerify = false + debug.error('服务初始化失败,将在1分钟后重试,错误:', err.message) + reject(err) + setTimeout(verifyMailClient, 60 * 1000) + } else { + isVerify = true + debug.success('服务启动成功') + resolve() + } + }) + }) +} + +/** + * @desc 发送邮件 + * @param {Object} opt={} 邮件参数 + * @param {Boolean} toMe=false 是否是给自己发送邮件 + */ +exports.send = (opt = {}, toMe = false) => { + if (!isVerify) { + return debug.error('客户端未验证,拒绝发送邮件') + } + opt.from = `${config.author} <${config.email}>` + if (toMe) { + opt.to = config.email + } + transporter.sendMail(opt, (err, info) => { + if (err) { + return debug.error('邮件发送失败,错误:', err.message) + } + debug.success('邮件发送成功', info.messageId, info.response) + }) +} + diff --git a/server/mongo.js b/server/plugins/mongo.js similarity index 88% rename from server/mongo.js rename to server/plugins/mongo.js index 81df8eb..c15f194 100644 --- a/server/mongo.js +++ b/server/plugins/mongo.js @@ -6,11 +6,11 @@ 'use strict' -const config = require('./config') +const config = require('../config') const mongoose = require('mongoose') -const { bhash, getDebug } = require('./util') -const { UserModel, OptionModel } = require('./model') -const { getGithubUsersInfo } = require('./service') +const { bhash, getDebug, proxy } = require('../util') +const { UserModel, OptionModel } = require('../model') +const { getGithubUsersInfo } = require('../service') const debug = getDebug('MongoDB') let isConnected = false @@ -66,7 +66,7 @@ async function createAdmin () { name: data.name, password: bhash(config.auth.defaultPassword), slogan: data.bio, - avatar: data.avatar_url, + avatar: proxy(data.avatar_url), github: { id: data.id, email: data.email, diff --git a/server/redis.js b/server/plugins/redis.js similarity index 94% rename from server/redis.js rename to server/plugins/redis.js index 9d0a2e3..4f89083 100644 --- a/server/redis.js +++ b/server/plugins/redis.js @@ -7,8 +7,8 @@ 'use strict' const redis = require('redis') -const config = require('./config') -const { getDebug, isType } = require('./util') +const config = require('../config') +const { getDebug, isType } = require('../util') const debug = getDebug('Redis') let client = null const cache = {} diff --git a/server/validation.js b/server/plugins/validation.js similarity index 96% rename from server/validation.js rename to server/plugins/validation.js index c6037f1..ac050fc 100644 --- a/server/validation.js +++ b/server/plugins/validation.js @@ -8,7 +8,7 @@ const mongoose = require('mongoose') const Validator = require('koa-bouncer').Validator -const { isObjectId } = require('./util') +const { isObjectId } = require('../util') Validator.addMethod('notEmpty', function (tip) { this.isString(`the "${this.key}" parameter should be String type`) diff --git a/server/service/crontab.js b/server/service/crontab.js index 757e1bb..6838d36 100644 --- a/server/service/crontab.js +++ b/server/service/crontab.js @@ -7,7 +7,7 @@ 'use strict' exports.start = () => { - const { option, music } = require('../controller') + const { option, music, user } = require('../controller') // 友链 每1小时更新一次 option.updateOptionLinks() setInterval(option.updateOptionLinks.bind(option), 1000 * 60 * 60 * 1) @@ -15,4 +15,8 @@ exports.start = () => { // 音乐 每10分钟更新一次 music.updateMusicCache() setInterval(music.updateMusicCache.bind(music), 1000 * 60 * 10) + + // 用户 每1天更新一次 + user.updateGithubInfo() + setInterval(user.updateGithubInfo.bind(user), 1000 * 60 * 60 * 24) } diff --git a/server/service/github-passport.js b/server/service/github-passport.js index 947f2b6..ed357c4 100644 --- a/server/service/github-passport.js +++ b/server/service/github-passport.js @@ -10,7 +10,7 @@ const passport = require('koa-passport') const GithubStrategy = require('passport-github').Strategy const config = require('../config') const { clientID, clientSecret, callbackURL } = config.sns.github -const { randomString, getDebug } = require('../util') +const { randomString, getDebug, proxy } = require('../util') const debug = getDebug('Github:Auth') exports.init = (UserModel, config) => { @@ -32,7 +32,7 @@ exports.init = (UserModel, config) => { if (user) { const userData = { name: profile.displayName || profile.username, - avatar: profile._json.avatar_url, + avatar: proxy(profile._json.avatar_url), slogan: profile._json.bio, github: profile._json, role: user.role @@ -49,7 +49,7 @@ exports.init = (UserModel, config) => { const newUser = { name: profile.displayName || profile.username, - avatar: profile._json.avatar_url, + avatar: proxy(profile._json.avatar_url), slogan: profile._json.bio, github: profile._json, role: 1 diff --git a/server/util/index.js b/server/util/index.js index 67e3ca0..a2722bf 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -33,7 +33,9 @@ exports.isType = (obj = {}, type = 'Object') => { }) } -exports.createObjectId = () => mongoose.Types.ObjectId() +exports.createObjectId = (id = '') => { + return id ? mongoose.Types.ObjectId(id) : mongoose.Types.ObjectId() +} exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) From a9c67bc339dac4f050dae28bd7fe9c1dc7a246f1 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 29 Oct 2017 20:23:22 +0800 Subject: [PATCH 052/208] [fix] fix bug (updating music list redis cache is not work) --- server/controller/music.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index 98a91f5..97528df 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -42,10 +42,12 @@ exports.list = async (ctx, next) => { const playListId = option.musicId const musicData = await redis.get(cacheKey) - if (musicData && musicData.id && musicData.list) { - return ctx.success(musicData.list) + // hit + if (musicData && musicData.id === playListId) { + return ctx.success(musicData.list || []) } + // update cache const data = await exports.updateMusicCache(playListId) ctx.success(data) } From 6710cfed2d797c864818a6476d0407371cf1d41e Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 29 Oct 2017 21:18:05 +0800 Subject: [PATCH 053/208] [update] fix somg bugs and update music api --- .gitignore | 5 + README.md | 4 +- ecosystem.config.js | 39 - package-lock.json | 3758 ---------------------------------- package.json | 1 + server/config/development.js | 5 - server/config/index.js | 6 +- server/config/production.js | 5 - server/controller/music.js | 6 +- server/controller/user.js | 5 +- server/plugins/mailer.js | 6 +- 11 files changed, 22 insertions(+), 3818 deletions(-) delete mode 100644 ecosystem.config.js delete mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 44b908e..06a2ae7 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,8 @@ npm-debug.log .idea .DS_Store +package-lock.json + +# pm2 +ecosystem.config.js +process.js diff --git a/README.md b/README.md index b111c7c..19fdb3f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## jooger.me-server -My blog's api server build by koa2 and mongoose +⚡️ My blog's api server build with koa2 and mongoose ## TODOS @@ -31,3 +31,5 @@ My blog's api server build by koa2 and mongoose * 日志api * 统计api + +* GC优化 diff --git a/ecosystem.config.js b/ecosystem.config.js deleted file mode 100644 index 21dd9e5..0000000 --- a/ecosystem.config.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @desc PM2 - * @author Jooger - * @date 26 Sep 2017 - */ - -'use strict' - -const packageInfo = require('./package.json') - -module.exports = { - apps: { - name: packageInfo.name, - script: './bin/www', - cwd: __dirname, - watch: false, - ignore_watch: ['[\/\\]\./', 'node_modules'], - env: { - NODE_ENV: 'production' - }, - env_production: { - NODE_ENV: 'production' - }, - log_date_format: 'YYYY-MM-DD HH:mm Z', - out_file: './logs/pm2-out.log', - error_file: './logs/pm2-error.log', - pid_file: './logs/jooger.me-server.pid' - }, - deploy: { - production: { - user : 'root', - host : 'jooger.me', - ref : 'origin/master', - repo : packageInfo.repository.url, - path : '/root/www/' + packageInfo.name, - 'post-deploy' : 'git pull && cnpm install && pm2 startOrReload ecosystem.config.js' - } - } -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 48dd543..0000000 --- a/package-lock.json +++ /dev/null @@ -1,3758 +0,0 @@ -{ - "name": "jooger.me-server", - "version": "1.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", - "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", - "dev": true - }, - "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", - "requires": { - "mime-types": "2.1.17", - "negotiator": "0.6.1" - } - }, - "akismet-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/akismet-api/-/akismet-api-3.0.0.tgz", - "integrity": "sha512-Kg0tgNFa8PX85cwZ+KinKfs/NDbRmXu7TZ/3mu4z/tBAwJosiqATEzmOGJLLDWfiXK01qHQJh6Ro6qRWpMp6Dg==", - "requires": { - "bluebird": "3.5.1", - "superagent": "3.8.0" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" - }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "requires": { - "string-width": "2.1.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" - }, - "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "dev": true, - "requires": { - "micromatch": "2.3.11", - "normalize-path": "2.1.1" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "1.1.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "axios": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", - "integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=", - "requires": { - "follow-redirects": "1.2.4", - "is-buffer": "1.1.5" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base64url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", - "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" - }, - "bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" - }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "requires": { - "callsite": "1.0.0" - } - }, - "big-integer": { - "version": "1.6.25", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.25.tgz", - "integrity": "sha1-HeRan1dUKsIBIcaC+NZCIgo06CM=" - }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "requires": { - "buffers": "0.1.1", - "chainsaw": "0.1.0" - } - }, - "binary-extensions": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", - "integrity": "sha1-muuabF6IY4qtFx4Wf1kAq+JINdA=", - "dev": true - }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, - "boxen": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.2.1.tgz", - "integrity": "sha1-DxHn/jRO25OXl3/BPt5/ZNlWSB0=", - "dev": true, - "requires": { - "ansi-align": "2.0.0", - "camelcase": "4.1.0", - "chalk": "2.1.0", - "cli-boxes": "1.0.0", - "string-width": "2.1.1", - "term-size": "1.2.0", - "widest-line": "1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.0" - } - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "chalk": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", - "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.4.0" - } - }, - "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" - } - }, - "bson": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", - "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "buffer-shims": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" - }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" - }, - "bunyan": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", - "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", - "requires": { - "dtrace-provider": "0.6.0", - "mv": "2.1.1", - "safe-json-stringify": "1.0.4" - } - }, - "bytes": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", - "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" - }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" - }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" - }, - "capture-stack-trace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", - "dev": true - }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "requires": { - "traverse": "0.3.9" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "dev": true, - "requires": { - "anymatch": "1.3.2", - "async-each": "1.0.1", - "fsevents": "1.1.2", - "glob-parent": "2.0.0", - "inherits": "2.0.3", - "is-binary-path": "1.0.1", - "is-glob": "2.0.1", - "path-is-absolute": "1.0.1", - "readdirp": "2.1.0" - } - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "co-body": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/co-body/-/co-body-4.2.0.tgz", - "integrity": "sha1-dN8g+nMmISXcRUgq8E40LqjbNRU=", - "requires": { - "inflation": "2.0.0", - "qs": "4.0.0", - "raw-body": "2.1.7", - "type-is": "1.6.15" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "color-convert": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", - "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" - }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "compressible": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.11.tgz", - "integrity": "sha1-FnGKdd4oPtjmBAQWJaIGRYZ5fYo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "configstore": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.1.tgz", - "integrity": "sha512-5oNkD/L++l0O6xGXxb1EWS7SivtjfGQlRyxJsYgE0Z495/L81e2h4/d3r969hoPXuFItzNOKMtsXgYG4c7dYvw==", - "dev": true, - "requires": { - "dot-prop": "4.2.0", - "graceful-fs": "4.1.11", - "make-dir": "1.0.0", - "unique-string": "1.0.0", - "write-file-atomic": "2.3.0", - "xdg-basedir": "3.0.0" - } - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookiejar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz", - "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=" - }, - "cookies": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.1.tgz", - "integrity": "sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=", - "requires": { - "depd": "1.1.1", - "keygrip": "1.0.2" - } - }, - "copy-to": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", - "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "crc": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz", - "integrity": "sha1-mLi6fUiWZbo5efWbITgTdBAaGWQ=" - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "1.0.0" - } - }, - "cross-env": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.0.5.tgz", - "integrity": "sha1-Q4PTZNlmCHPdGFs5ivO/717//vM=", - "dev": true, - "requires": { - "cross-spawn": "5.1.0", - "is-windows": "1.0.1" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "4.1.1", - "shebang-command": "1.2.0", - "which": "1.3.0" - } - }, - "crypto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", - "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "1.0.1" - } - }, - "double-ended-queue": { - "version": "2.1.0-0", - "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", - "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" - }, - "dtrace-provider": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", - "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", - "optional": true, - "requires": { - "nan": "2.7.0" - } - }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "ecdsa-sig-formatter": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", - "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", - "requires": { - "base64url": "2.0.0", - "safe-buffer": "5.1.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "error-inject": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", - "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" - }, - "es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", - "dev": true, - "requires": { - "duplexer": "0.1.1", - "from": "0.1.7", - "map-stream": "0.1.0", - "pause-stream": "0.0.11", - "split": "0.3.3", - "stream-combiner": "0.0.4", - "through": "2.3.8" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" - } - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "0.1.1" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "2.2.3" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fill-range": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", - "dev": true, - "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "1.1.7", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" - } - }, - "follow-redirects": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.2.4.tgz", - "integrity": "sha512-Suw6KewLV2hReSyEOeql+UUkBVyiBm3ok1VPrVFRZnQInWpdoZbbiG5i8aJVSjTr0yQ4Ava0Sh6/joCg1Brdqw==", - "requires": { - "debug": "2.6.9" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "1.0.2" - } - }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "formidable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.1.1.tgz", - "integrity": "sha1-lriIb3w8NQi5Mta9cMTTqI818ak=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", - "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.7.0", - "node-pre-gyp": "0.6.36" - }, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.2.9" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "2.6.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true, - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.15" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.1" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.1.1", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true, - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.0", - "sshpk": "1.13.0" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.4", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "1.27.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.36", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.0", - "rc": "1.2.1", - "request": "2.81.0", - "rimraf": "2.6.1", - "semver": "5.3.0", - "tar": "2.2.1", - "tar-pack": "3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.0", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.1", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.15", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.0.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.6.0", - "uuid": "3.0.1" - } - }, - "rimraf": { - "version": "2.6.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.3.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jodid25519": "1.0.2", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.8", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.2.9", - "rimraf": "2.6.1", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "verror": { - "version": "1.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - } - } - }, - "fstream": { - "version": "0.1.31", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-0.1.31.tgz", - "integrity": "sha1-czfwWPu7vvqMn1YaKMqwhJICyYg=", - "requires": { - "graceful-fs": "3.0.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.4.5" - }, - "dependencies": { - "graceful-fs": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", - "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", - "requires": { - "natives": "1.1.0" - } - } - } - }, - "geoip-lite": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/geoip-lite/-/geoip-lite-1.2.1.tgz", - "integrity": "sha1-OcSO+T5JiiWezka8npjPPdtoDYs=", - "requires": { - "async": "2.5.0", - "colors": "1.1.2", - "glob": "7.1.2", - "iconv-lite": "0.4.13", - "lazy": "1.0.11", - "rimraf": "2.6.2", - "unzip": "0.1.11" - }, - "dependencies": { - "async": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", - "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", - "requires": { - "lodash": "4.17.4" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - } - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "2.0.1" - } - }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "3.0.2", - "duplexer3": "0.1.4", - "get-stream": "3.0.0", - "is-redirect": "1.0.0", - "is-retry-allowed": "1.1.0", - "is-stream": "1.1.0", - "lowercase-keys": "1.0.0", - "safe-buffer": "5.1.1", - "timed-out": "4.0.1", - "unzip-response": "2.0.1", - "url-parse-lax": "1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "highlight.js": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", - "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=" - }, - "hooks-fixed": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz", - "integrity": "sha1-oB2JTVKsf2WZu7H2PfycQR33DLo=" - }, - "http-assert": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.3.0.tgz", - "integrity": "sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=", - "requires": { - "deep-equal": "1.0.1", - "http-errors": "1.6.2" - } - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.3.1" - } - }, - "humanize-number": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/humanize-number/-/humanize-number-0.0.2.tgz", - "integrity": "sha1-EcCvakcWQ2M1iFiASPF5lUFInBg=" - }, - "iconv-lite": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", - "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=" - }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflation": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", - "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "1.10.0" - } - }, - "is-buffer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" - }, - "is-class": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/is-class/-/is-class-0.0.4.tgz", - "integrity": "sha1-4FdFFwW7NOOePjNZjJOpg3KWtzY=" - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-generator-function": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.6.tgz", - "integrity": "sha1-nnFlPNFf/zQcecQVFGChMdMen8Q=" - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-type-of": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-type-of/-/is-type-of-1.2.0.tgz", - "integrity": "sha512-10ezBXuEDp3Fp/jPCaVd4hSrAEj2lPyr1LT7+cWi9HCLd15wbh9X8dJfTDB+ZgkZSCGTG2TF6f61ugI5mSlhDA==", - "requires": { - "core-util-is": "1.0.2", - "is-class": "0.0.4", - "isstream": "0.1.2" - } - }, - "is-windows": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.1.tgz", - "integrity": "sha1-MQ23D3QtJZoWo2kgK1GvhCMzENk=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonwebtoken": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz", - "integrity": "sha1-xjl80uX9WD1lwAeoPce7eOaYK4M=", - "requires": { - "jws": "3.1.4", - "lodash.includes": "4.3.0", - "lodash.isboolean": "3.0.3", - "lodash.isinteger": "4.0.4", - "lodash.isnumber": "3.0.3", - "lodash.isplainobject": "4.0.6", - "lodash.isstring": "4.0.1", - "lodash.once": "4.1.1", - "ms": "2.0.0", - "xtend": "4.0.1" - } - }, - "jwa": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", - "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", - "requires": { - "base64url": "2.0.0", - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.9", - "safe-buffer": "5.1.1" - } - }, - "jws": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", - "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", - "requires": { - "base64url": "2.0.0", - "jwa": "1.1.5", - "safe-buffer": "5.1.1" - } - }, - "kareem": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz", - "integrity": "sha1-4+QQHZ3P3imXadr0tNtk2JXRdEg=" - }, - "keygrip": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", - "integrity": "sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=" - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.5" - } - }, - "koa": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.3.0.tgz", - "integrity": "sha1-nh6OTaQBg5xXuFJ+rcV/dhJ1Vac=", - "requires": { - "accepts": "1.3.4", - "content-disposition": "0.5.2", - "content-type": "1.0.4", - "cookies": "0.7.1", - "debug": "2.6.9", - "delegates": "1.0.0", - "depd": "1.1.1", - "destroy": "1.0.4", - "error-inject": "1.0.0", - "escape-html": "1.0.3", - "fresh": "0.5.2", - "http-assert": "1.3.0", - "http-errors": "1.6.2", - "is-generator-function": "1.0.6", - "koa-compose": "4.0.0", - "koa-convert": "1.2.0", - "koa-is-json": "1.0.0", - "mime-types": "2.1.17", - "on-finished": "2.3.0", - "only": "0.0.2", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "type-is": "1.6.15", - "vary": "1.1.2" - } - }, - "koa-bodyparser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-3.2.0.tgz", - "integrity": "sha1-uRbeF+IDn+gmUEgZc9fClPELVxk=", - "requires": { - "co-body": "4.2.0" - } - }, - "koa-bouncer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/koa-bouncer/-/koa-bouncer-6.0.0.tgz", - "integrity": "sha1-XV9WJzYnU1nzpaR19DVbTxqxto0=", - "requires": { - "better-assert": "1.0.2", - "debug": "2.6.9", - "lodash": "4.17.4", - "validator": "4.9.0" - } - }, - "koa-bunyan-logger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/koa-bunyan-logger/-/koa-bunyan-logger-2.0.0.tgz", - "integrity": "sha1-TtkDR+mHhJ9JU/kVXeRvl/WFRM0=", - "requires": { - "bunyan": "1.5.1", - "on-finished": "2.1.1", - "uuid": "3.1.0" - }, - "dependencies": { - "ee-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", - "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" - }, - "on-finished": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", - "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=", - "requires": { - "ee-first": "1.1.0" - } - } - } - }, - "koa-compose": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.0.0.tgz", - "integrity": "sha1-KAClE9nDYe8NY4UrA45Pby1adzw=" - }, - "koa-compress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/koa-compress/-/koa-compress-2.0.0.tgz", - "integrity": "sha1-e36ykhuEd0a14SK6n1zYpnHo6jo=", - "requires": { - "bytes": "2.4.0", - "compressible": "2.0.11", - "koa-is-json": "1.0.0", - "statuses": "1.3.1" - } - }, - "koa-convert": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", - "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", - "requires": { - "co": "4.6.0", - "koa-compose": "3.2.1" - }, - "dependencies": { - "koa-compose": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", - "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", - "requires": { - "any-promise": "1.3.0" - } - } - } - }, - "koa-is-json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", - "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" - }, - "koa-json": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/koa-json/-/koa-json-2.0.2.tgz", - "integrity": "sha1-Nq8U5uofXWRtfESihXAcb4Wk/eQ=", - "requires": { - "koa-is-json": "1.0.0", - "streaming-json-stringify": "3.1.0" - } - }, - "koa-logger": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/koa-logger/-/koa-logger-2.0.1.tgz", - "integrity": "sha1-PuQvRXxA+01KGaExyAIlBLWyMrg=", - "requires": { - "bytes": "1.0.0", - "chalk": "1.1.3", - "humanize-number": "0.0.2", - "passthrough-counter": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=" - } - } - }, - "koa-onerror": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/koa-onerror/-/koa-onerror-1.3.1.tgz", - "integrity": "sha1-pbVh4ch+8yieQwF0bgE4LyT/viI=", - "requires": { - "copy-to": "2.0.1", - "swig": "1.4.2" - } - }, - "koa-passport": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/koa-passport/-/koa-passport-4.0.0.tgz", - "integrity": "sha512-o7KHuioB7VPiAoEFiu1W3CRtiBTWilfBGbTQi4cb5DIbPwETG0kfC8q2b9PuPIhSt3BVN4gF3o6Qu3VsdiuwLg==", - "requires": { - "passport": "0.4.0" - } - }, - "koa-router": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.2.1.tgz", - "integrity": "sha1-tApKs8attLQIld69AKnGQDBOMDk=", - "requires": { - "debug": "2.6.9", - "http-errors": "1.6.2", - "koa-compose": "3.2.1", - "methods": "1.1.2", - "path-to-regexp": "1.7.0" - }, - "dependencies": { - "koa-compose": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", - "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", - "requires": { - "any-promise": "1.3.0" - } - } - } - }, - "koa-session": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/koa-session/-/koa-session-5.5.0.tgz", - "integrity": "sha512-+f5YY137wuu4RtSaalWJdUYd80S1v79uWcecIRGLKVtljTuv6fFzPlvTmWl1V0MaSin8rEXP9urgbIVQEN/YVA==", - "requires": { - "crc": "3.5.0", - "debug": "2.6.9", - "is-type-of": "1.2.0", - "uid-safe": "2.1.5" - } - }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "requires": { - "package-json": "4.0.1" - } - }, - "lazy": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz", - "integrity": "sha1-2qBoIGKCVCwIgojpdcKXwa53tpA=" - }, - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "3.0.1", - "lodash.keys": "3.1.2" - } - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._bindcallback": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", - "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", - "dev": true - }, - "lodash._createassigner": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", - "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", - "dev": true, - "requires": { - "lodash._bindcallback": "3.0.1", - "lodash._isiterateecall": "3.0.9", - "lodash.restparam": "3.6.1" - } - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "lodash.assign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", - "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", - "dev": true, - "requires": { - "lodash._baseassign": "3.2.0", - "lodash._createassigner": "3.1.1", - "lodash.keys": "3.1.2" - } - }, - "lodash.defaults": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", - "integrity": "sha1-xzCLGNv4vJNy1wGnNJPGEZK9Liw=", - "dev": true, - "requires": { - "lodash.assign": "3.2.0", - "lodash.restparam": "3.6.1" - } - }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "3.9.1", - "lodash.isarguments": "3.1.0", - "lodash.isarray": "3.0.4" - } - }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" - }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", - "dev": true - }, - "lowercase-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", - "dev": true - }, - "lru-cache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", - "dev": true, - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - }, - "make-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.0.0.tgz", - "integrity": "sha1-l6ARdR6R3YfPre9Ygy67BJNt6Xg=", - "dev": true, - "requires": { - "pify": "2.3.0" - } - }, - "map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", - "dev": true - }, - "marked": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", - "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=" - }, - "match-stream": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/match-stream/-/match-stream-0.0.2.tgz", - "integrity": "sha1-mesFAJOzTf+t5CG5rAtBCpz6F88=", - "requires": { - "buffers": "0.1.1", - "readable-stream": "1.0.34" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" - } - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.8" - } - }, - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, - "mongodb": { - "version": "2.2.33", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.33.tgz", - "integrity": "sha1-tTfEcdNKZlG0jzb9vyl1A0Dgi1A=", - "requires": { - "es6-promise": "3.2.1", - "mongodb-core": "2.1.17", - "readable-stream": "2.2.7" - }, - "dependencies": { - "es6-promise": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", - "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" - }, - "readable-stream": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", - "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - } - } - }, - "mongodb-core": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.17.tgz", - "integrity": "sha1-pBizN6FKFJkPtRC5I97mqBMXPfg=", - "requires": { - "bson": "1.0.4", - "require_optional": "1.0.1" - } - }, - "mongoose": { - "version": "4.12.4", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.12.4.tgz", - "integrity": "sha512-3QTbQ/+wRe8Lr1IGTBAu2gyx+7VgvnCGmY2N1HSW8nsvfMGO0QW9iNDzZT3ceEY2Wctlt28wNavHivcLDO76bQ==", - "requires": { - "async": "2.1.4", - "bson": "1.0.4", - "hooks-fixed": "2.0.0", - "kareem": "1.5.0", - "mongodb": "2.2.33", - "mpath": "0.3.0", - "mpromise": "0.5.5", - "mquery": "2.3.2", - "ms": "2.0.0", - "muri": "1.3.0", - "regexp-clone": "0.0.1", - "sliced": "1.0.1" - }, - "dependencies": { - "async": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", - "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=", - "requires": { - "lodash": "4.17.4" - } - } - } - }, - "mongoose-paginate": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/mongoose-paginate/-/mongoose-paginate-5.0.3.tgz", - "integrity": "sha1-165J7Vv2Tx9692IOqGW2cFjFU3E=", - "requires": { - "bluebird": "3.0.5" - }, - "dependencies": { - "bluebird": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.0.5.tgz", - "integrity": "sha1-L/nQfJs+2ynW0oD+B1KDZefs05I=" - } - } - }, - "mpath": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz", - "integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q=" - }, - "mpromise": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz", - "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" - }, - "mquery": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.2.tgz", - "integrity": "sha512-KXWMypZSvhCuqRtza+HMQZdYw7PfFBjBTFvP31NNAq0OX0/NTIgpcDpkWQ2uTxk6vGQtwQ2elhwhs+ZvCA8OaA==", - "requires": { - "bluebird": "3.5.1", - "debug": "2.6.9", - "regexp-clone": "0.0.1", - "sliced": "0.0.5" - }, - "dependencies": { - "sliced": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", - "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "muri": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/muri/-/muri-1.3.0.tgz", - "integrity": "sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg==" - }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "optional": true, - "requires": { - "mkdirp": "0.5.1", - "ncp": "2.0.0", - "rimraf": "2.4.5" - } - }, - "nan": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", - "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=", - "optional": true - }, - "natives": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.0.tgz", - "integrity": "sha1-6f+EFBimsux6SV6TmYT3jxY+bjE=" - }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", - "optional": true - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "nodemailer": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.3.1.tgz", - "integrity": "sha512-ngyDzou/Rbppn9WUOpWNoe25mRrW5wMwRokWanBNLt+3YaxOLmUtrc0ZtHMOgHGFPAYNgKA9H70ELlV3qSHL7Q==" - }, - "nodemon": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.12.1.tgz", - "integrity": "sha1-mWpW3EnZ8Wu/G3ik3gjxNjSzh40=", - "dev": true, - "requires": { - "chokidar": "1.7.0", - "debug": "2.6.9", - "es6-promise": "3.3.1", - "ignore-by-default": "1.0.1", - "lodash.defaults": "3.1.2", - "minimatch": "3.0.4", - "ps-tree": "1.1.0", - "touch": "3.1.0", - "undefsafe": "0.0.3", - "update-notifier": "2.2.0" - } - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1.1.0" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "1.1.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "2.0.1" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" - } - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" - } - }, - "over": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/over/-/over-0.0.5.tgz", - "integrity": "sha1-8phS5w/X4l82DgE6jsRMgq7bVwg=" - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "6.7.1", - "registry-auth-token": "3.3.1", - "registry-url": "3.1.0", - "semver": "5.4.1" - } - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "passport": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", - "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", - "requires": { - "passport-strategy": "1.0.0", - "pause": "0.0.1" - } - }, - "passport-github": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz", - "integrity": "sha1-jOHj/NYa11eOsd9ZWDnkrqEjVdQ=", - "requires": { - "passport-oauth2": "1.4.0" - } - }, - "passport-oauth2": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", - "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", - "requires": { - "oauth": "0.9.15", - "passport-strategy": "1.0.0", - "uid2": "0.0.3", - "utils-merge": "1.0.1" - } - }, - "passport-strategy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" - }, - "passthrough-counter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passthrough-counter/-/passthrough-counter-1.0.0.tgz", - "integrity": "sha1-GWfZ5m2lcrXAI8eH2xEqOHqxZvo=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - } - } - }, - "pause": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" - }, - "pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", - "dev": true, - "requires": { - "through": "2.3.8" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "ps-tree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", - "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", - "dev": true, - "requires": { - "event-stream": "3.3.4" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "pullstream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pullstream/-/pullstream-0.4.1.tgz", - "integrity": "sha1-1vs79a7Wl+gxFQ6xACwlo/iuExQ=", - "requires": { - "over": "0.0.5", - "readable-stream": "1.0.34", - "setimmediate": "1.0.5", - "slice-stream": "1.0.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "qs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz", - "integrity": "sha1-wx2bdOwn33XlQ6hseHKO2NRiNgc=" - }, - "random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" - }, - "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", - "dev": true, - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "1.1.5" - } - } - } - }, - "raw-body": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", - "integrity": "sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q=", - "requires": { - "bytes": "2.4.0", - "iconv-lite": "0.4.13", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", - "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", - "dev": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "minimatch": "3.0.4", - "readable-stream": "2.3.3", - "set-immediate-shim": "1.0.1" - } - }, - "redis": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", - "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", - "requires": { - "double-ended-queue": "2.1.0-0", - "redis-commands": "1.3.1", - "redis-parser": "2.6.0" - } - }, - "redis-commands": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz", - "integrity": "sha1-gdgm9F+pyLIBH0zXoP5ZfSQdRCs=" - }, - "redis-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", - "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "0.1.3" - } - }, - "regexp-clone": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", - "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" - }, - "registry-auth-token": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.1.tgz", - "integrity": "sha1-+w0yie4Nmtosu1KvXf5mywcNMAY=", - "dev": true, - "requires": { - "rc": "1.2.1", - "safe-buffer": "5.1.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "1.2.1" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "2.0.0", - "semver": "5.4.1" - } - }, - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "requires": { - "glob": "6.0.4" - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "safe-json-stringify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz", - "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", - "optional": true - }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "requires": { - "semver": "5.4.1" - } - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "simple-netease-cloud-music": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/simple-netease-cloud-music/-/simple-netease-cloud-music-0.1.8.tgz", - "integrity": "sha512-1vTRDzk0TYEUWrqdiUMl/cobJPj67YBmEaBfqqjnl+epC1ubhynqeqppy8bDIVX4giPItOWaI7ugz60rY1q+TA==" - }, - "slice-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-stream/-/slice-stream-1.0.0.tgz", - "integrity": "sha1-WzO9ZvATsaf4ZGCwPUY97DmtPqA=", - "requires": { - "readable-stream": "1.0.34" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "sliced": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", - "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" - }, - "source-map": { - "version": "0.1.34", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", - "integrity": "sha1-p8/omux7FoLDsZjQrPtH19CQVms=", - "requires": { - "amdefine": "1.0.1" - } - }, - "split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", - "dev": true, - "requires": { - "through": "2.3.8" - } - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", - "dev": true, - "requires": { - "duplexer": "0.1.1" - } - }, - "streaming-json-stringify": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/streaming-json-stringify/-/streaming-json-stringify-3.1.0.tgz", - "integrity": "sha1-gCAEN6mTzDnE/gAmO3s7kDrIevU=", - "requires": { - "json-stringify-safe": "5.0.1", - "readable-stream": "2.3.3" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "superagent": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.0.tgz", - "integrity": "sha512-71XGWgtn70TNwgmgYa69dPOYg55aU9FCahjUNY03rOrKvaTCaU3b9MeZmqonmf9Od96SCxr3vGfEAnhM7dtxCw==", - "requires": { - "component-emitter": "1.2.1", - "cookiejar": "2.1.1", - "debug": "3.1.0", - "extend": "3.0.1", - "form-data": "2.3.1", - "formidable": "1.1.1", - "methods": "1.1.2", - "mime": "1.4.1", - "qs": "6.5.1", - "readable-stream": "2.3.3" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - } - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "swig": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/swig/-/swig-1.4.2.tgz", - "integrity": "sha1-QIXKBFM2kQS11IPihBs5t64aq6U=", - "requires": { - "optimist": "0.6.1", - "uglify-js": "2.4.24" - } - }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true, - "requires": { - "execa": "0.7.0" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true - }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "1.0.10" - } - }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" - }, - "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", - "requires": { - "media-typer": "0.3.0", - "mime-types": "2.1.17" - } - }, - "uglify-js": { - "version": "2.4.24", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", - "integrity": "sha1-+tV1XB4Vd2WLsG/5q25UjJW+vW4=", - "requires": { - "async": "0.2.10", - "source-map": "0.1.34", - "uglify-to-browserify": "1.0.2", - "yargs": "3.5.4" - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=" - }, - "uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "requires": { - "random-bytes": "1.0.0" - } - }, - "uid2": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", - "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" - }, - "undefsafe": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-0.0.3.tgz", - "integrity": "sha1-7Mo6A+VrmvFzhbqsgSrIO5lKli8=", - "dev": true - }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "1.0.0" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "unzip": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/unzip/-/unzip-0.1.11.tgz", - "integrity": "sha1-iXScY7BY19kNYZ+GuYqhU107l/A=", - "requires": { - "binary": "0.3.0", - "fstream": "0.1.31", - "match-stream": "0.0.2", - "pullstream": "0.4.1", - "readable-stream": "1.0.34", - "setimmediate": "1.0.5" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "update-notifier": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.2.0.tgz", - "integrity": "sha1-G1g3z5DAc22IYncytmHBOPht5y8=", - "dev": true, - "requires": { - "boxen": "1.2.1", - "chalk": "1.1.3", - "configstore": "3.1.1", - "import-lazy": "2.1.0", - "is-npm": "1.0.0", - "latest-version": "3.1.0", - "semver-diff": "2.1.0", - "xdg-basedir": "3.0.0" - } - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, - "requires": { - "prepend-http": "1.0.4" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" - }, - "validator": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-4.9.0.tgz", - "integrity": "sha1-CC/84qdhSP8HqOienCukOq8S7Ew=", - "requires": { - "depd": "1.1.0" - }, - "dependencies": { - "depd": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", - "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" - } - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "dev": true, - "requires": { - "isexe": "2.0.0" - } - }, - "widest-line": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz", - "integrity": "sha1-DAnIXCqUaD0Nfq+O4JfVZL8OEFw=", - "dev": true, - "requires": { - "string-width": "1.0.2" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "signal-exit": "3.0.2" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yargs": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", - "integrity": "sha1-2K/49mXpTDS9JZvevRv68N3TU2E=", - "requires": { - "camelcase": "1.2.1", - "decamelize": "1.2.0", - "window-size": "0.1.0", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" - } - } - } - } -} diff --git a/package.json b/package.json index f526089..43c5daf 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "debug": "cross-env NODE_ENV=development nodemon --inspect bin/www", "prod": "cross-env NODE_ENV=production nodemon bin/www", "pm2": "pm2 startOrReload ecosystem.config.js", + "pm2:prod": "pm2 startOrReload ecosystem.config.js --env production", "test": "echo \"Error: no test specified\" && exit 1", "deploy": "pm2 deploy ecosystem.config.js production" }, diff --git a/server/config/development.js b/server/config/development.js index 0c3f6ba..99f9c42 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -14,12 +14,7 @@ module.exports = { }, sns: { github: { - clientID: '5b4d4a7945347d0fd2e2', - clientSecret: '8771bd9ae52749cc15b0c9e2c6cb4ecd7f39d9da', callbackURL: 'http://127.0.0.1:3001/auth/github/login/callback' } - }, - akismet: { - apiKey: '7fa12f4a1d08' } } diff --git a/server/config/index.js b/server/config/index.js index d13ca6f..eb3e270 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -64,13 +64,13 @@ const baseConfig = { }, sns: { github: { - clientID: 'github client id', - clientSecret: 'github client secret', + clientID: process.env.githubClientID || 'github client id', + clientSecret: process.env.githubClientSecret || 'github client secret', callbackURL: 'github oauth callback url' } }, akismet: { - apiKey: 'akismet api key' + apiKey: process.env.akismetApikey || 'akismet api key' } } diff --git a/server/config/production.js b/server/config/production.js index 7464684..8ce7ba8 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -19,12 +19,7 @@ module.exports = { }, sns: { github: { - clientID: 'cc9133ad08a5fbc3b7bd', - clientSecret: '4b98cc1028eddc78e72d5e48657819be50581623', callbackURL: 'https://api.jooger.me/auth/github/login/callback' } - }, - akismet: { - apiKey: '7fa12f4a1d08' } } diff --git a/server/controller/music.js b/server/controller/music.js index 97528df..dec1d78 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -137,7 +137,7 @@ exports.updateMusicCache = async function (playListId = '') { debug.error('Option查找失败,错误:', err.message) return null }) - + if (!option || !option.musicId) { return debug.warn('歌单ID未配置') } @@ -151,9 +151,9 @@ exports.updateMusicCache = async function (playListId = '') { } redis.set(cacheKey, set).then(() => { - debug.success('缓存更新成功') + debug.success('缓存更新成功,歌单ID:', playListId) }).catch(err => { - debug.error('缓存更新失败,错误:', err.message) + debug.error('缓存更新失败,歌单ID:%s,错误:%s', playListId, err.message) }) lock = false diff --git a/server/controller/user.js b/server/controller/user.js index 093744b..251b9aa 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -150,6 +150,9 @@ exports.updateGithubInfo = async () => { const updates = await getGithubUsersInfo(users.map(user => user.github.login)) Promise.all( updates.map((data, index) => { + if (!data) { + return null + } const user = users[index] const u = { github: { @@ -169,7 +172,7 @@ exports.updateGithubInfo = async () => { return UserModel.findByIdAndUpdate(user._id, u).exec().catch(err => { debug.error('用户Github信息更新失败,错误:', err.message) }) - }) + }).filter(ps => !!ps) ).then(() => { debug.success('全部用户Github信息更新成功') }) diff --git a/server/plugins/mailer.js b/server/plugins/mailer.js index 02b6e5c..1426dd0 100644 --- a/server/plugins/mailer.js +++ b/server/plugins/mailer.js @@ -17,7 +17,7 @@ const transporter = nodemailer.createTransport({ secure: true, auth: { user: config.email, - pass: '19950102zzy' + pass: process.env['163Pass'] || '163邮箱密码' } }) @@ -26,9 +26,9 @@ exports.start = async () => { transporter.verify((err, success) => { if (err) { isVerify = false - debug.error('服务初始化失败,将在1分钟后重试,错误:', err.message) + debug.error('服务启动失败,将在1分钟后重试,错误:', err.message) reject(err) - setTimeout(verifyMailClient, 60 * 1000) + setTimeout(exports.start, 60 * 1000) } else { isVerify = true debug.success('服务启动成功') From d8b7654b3cd3d4c6c95d1dc2f80eac447a344210 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 29 Oct 2017 21:59:20 +0800 Subject: [PATCH 054/208] [update] update README --- README.md | 74 +++++++++++++++++++++++++++++++++++++- api.md | 2 +- package.json | 4 +-- server/controller/music.js | 2 +- 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 19fdb3f..003d5a6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,77 @@ ## jooger.me-server -⚡️ My blog's api server build with koa2 and mongoose +⚡️ My blog's api server build with koa2 and mongoose,a RESTful application. + +## Online site + +* jooger.me: [https://jooger.me](https://jooger.me) + +* jooger.me-server: [https://api.jooger.me](https://api.jooger.me) + +* jooger.me-admin: [https://admin.jooger.me](https://admin.jooger.me) + +## Build Setup + +``` bash +# install dependencies +$ npm install # Or yarn install + +# serve at localhost:3001 in development env +$ npm run dev + +# serve at localhost:3001 in production env +$ npm run prod + +# serve with pm2 in development env +$ npm run pm2 + +# serve with pm2 in production env +$npm run pm2:prod + +# run pm2 deploy, need ecosystem.config.js at root path +$ npm run deploy + +# test code (TODO) +$ npm run test +``` + +## Directory tree + +``` +jooger.me-server +|____api.md // api文档(带完善) +|____bin // 启动目录 +|____ecosystem.config.js // pm2启动文件,需要自己手动创建 +|____LICENSE // LICENSE(MIT) +|____logs // 日志目录,在ecosystem.config.js中配置 +|____server // 程序主目录 +| |____app.js // App程序入口 +| |____config // 配置文件目录 +| | |____development.js // 开发环境配置 +| | |____production.js // 生产环境配置 +| | |____test.js // 测试环境配置 +| |____controller // Controllers +| |____middleware // Koa中间件 +| |____model // 数据持久化模型 +| |____plugins // 插件目录 +| | |____akismet.js // 评论反垃圾 +| | |____mailer.js // 邮件客户端 +| | |____mongo.js // MongoDB驱动(mongoose) +| | |____redis.js // Redis +| | |____validation.js // 额外的校验规则 +| |____routes // 路由目录 +| | |____backend.js // 后台路由 +| | |____frontend.js // 前台路由 +| |____service // 服务目录 +| | |____crontab.js // 定时更新任务 +| | |____github-passport.js // Github验证 +| | |____github-userinfo.js // 获取Github用户信息 +| | |____netease-music.js // 网易云音乐api +| |____util // 常用工具 +|____test // 测试目录 + +``` ## TODOS @@ -33,3 +103,5 @@ * 统计api * GC优化 + +* 完善API文档 diff --git a/api.md b/api.md index dfde1d3..a24c3a9 100644 --- a/api.md +++ b/api.md @@ -1,6 +1,6 @@ ## Api -prefix: /api +Api文档 ### 文章 diff --git a/package.json b/package.json index 43c5daf..f67179d 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "prod": "cross-env NODE_ENV=production nodemon bin/www", "pm2": "pm2 startOrReload ecosystem.config.js", "pm2:prod": "pm2 startOrReload ecosystem.config.js --env production", - "test": "echo \"Error: no test specified\" && exit 1", - "deploy": "pm2 deploy ecosystem.config.js production" + "deploy": "pm2 deploy ecosystem.config.js production", + "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Jooger", "site": "https://jooger.me", diff --git a/server/controller/music.js b/server/controller/music.js index dec1d78..a0f6740 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -49,7 +49,7 @@ exports.list = async (ctx, next) => { // update cache const data = await exports.updateMusicCache(playListId) - ctx.success(data) + ctx.success(data.list) } } From 1056a2f5eb243e53d7cdfd06a21c702e8653cade Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 30 Oct 2017 01:09:59 +0800 Subject: [PATCH 055/208] [feature] add v8 gc optimize --- README.md | 4 ++-- package.json | 2 ++ server/app.js | 5 ++++- server/plugins/gc.js | 23 +++++++++++++++++++++++ server/plugins/index.js | 1 + 5 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 server/plugins/gc.js diff --git a/README.md b/README.md index 003d5a6..f4d6721 100644 --- a/README.md +++ b/README.md @@ -96,12 +96,12 @@ jooger.me-server * ~~评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer)~~ (2017.10.29) +* ~~GC优化~~ (2017.10.30) + * 消息api * 日志api * 统计api -* GC优化 - * 完善API文档 diff --git a/package.json b/package.json index f67179d..59ad56d 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,10 @@ "crypto": "^1.0.1", "debug": "^2.6.9", "formidable": "^1.1.1", + "gc-stats": "^1.0.2", "geoip-lite": "^1.2.1", "highlight.js": "^9.12.0", + "idle-gc": "^1.0.1", "jsonwebtoken": "^8.1.0", "koa": "^2.2.0", "koa-bodyparser": "^3.2.0", diff --git a/server/app.js b/server/app.js index dbf2825..da6d7d4 100644 --- a/server/app.js +++ b/server/app.js @@ -18,7 +18,7 @@ const bodyparser = require('koa-bodyparser') const koaBunyanLogger = require('koa-bunyan-logger') const middlewares = require('./middleware') const config = require('./config') -const { mongo, redis, akismet, validation, mailer } = require('./plugins') +const { mongo, redis, akismet, validation, mailer, gc } = require('./plugins') const { crontab } = require('./service') const app = new Koa() @@ -66,4 +66,7 @@ mailer.start() // crontab crontab.start() +// v8 gc +gc.start() + module.exports = app diff --git a/server/plugins/gc.js b/server/plugins/gc.js new file mode 100644 index 0000000..7e8f431 --- /dev/null +++ b/server/plugins/gc.js @@ -0,0 +1,23 @@ +/** + * @desc V8 GC + * @author Jooger + * @date 30 Oct 2017 + */ + +'use strict' + +const gc = require('idle-gc') +const gcStat = require('gc-stats') +const debug = require('../util').getDebug('GC') + +exports.start = (interval = 5000, delay = 5000) => { + setTimeout(() => { + gcStat().on('stats', stats => { + debug('回收完毕,用时 %s ms,共回收 %s KB堆内存', stats.pauseMS, stats.diff.totalHeapSize / 1000) + }) + gc.start(interval) + debug.success('服务启动成功') + }, delay) +} + +exports.stop = () => gc.stop() diff --git a/server/plugins/index.js b/server/plugins/index.js index f61aec9..57dec53 100644 --- a/server/plugins/index.js +++ b/server/plugins/index.js @@ -11,3 +11,4 @@ exports.redis = require('./redis') exports.akismet = require('./akismet') exports.validation = require('./validation') exports.mailer = require('./mailer') +exports.gc = require('./gc') From c86a1a5d3a25bd9b61351f566c94592efc4d3aab Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 30 Oct 2017 14:45:14 +0800 Subject: [PATCH 056/208] [fix] fix http proxy wrapper bug --- README.md | 2 +- server/controller/music.js | 17 ++++++++++++++--- server/util/proxy.js | 3 ++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f4d6721..b64d056 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ jooger.me-server * ~~评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer)~~ (2017.10.29) -* ~~GC优化~~ (2017.10.30) +* ~~GC优化~~ (2017.10.30,linux下需要预先安装g++) * 消息api diff --git a/server/controller/music.js b/server/controller/music.js index a0f6740..ccc799d 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -10,7 +10,7 @@ const NeteseMusic = require('simple-netease-cloud-music') const config = require('../config') const { fetchNE } = require('../service') const { OptionModel } = require('../model') -const { proxy, getDebug } = require('../util') +const { proxy, getDebug, isType } = require('../util') const { redis } = require('../plugins') const isProd = process.env.NODE_ENV === 'production' @@ -72,7 +72,18 @@ exports.url = async (ctx, next) => { .isString('the "song_id" parameter should be String type') .val() - const data = await neteaseMusic.url(songId) + const data = await neteaseMusic.url(songId).then(data => { + if (!isProd) { + return data.data || [] + } + if (isType(data.data, 'Array')) { + return data.data.map(item => { + item.url = proxy(item.url) + return item + }) + } + return [] + }) ctx.success(data) } @@ -111,7 +122,7 @@ function fetchSonglist (playListId) { duration: dt || 0, album: al && { name: al.name, - cover: isProd ? (al.picUrl ? `${config.site}${proxy(al.picUrl)}` : '') : al.picUrl, + cover: isProd ? (proxy(al.picUrl) || '') : al.picUrl, tns: al.tns } || {}, artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], diff --git a/server/util/proxy.js b/server/util/proxy.js index 037bf6f..df85c15 100644 --- a/server/util/proxy.js +++ b/server/util/proxy.js @@ -4,11 +4,12 @@ * @date 20 Oct 2017 */ +const config = require('../config') const prefix = 'http://' module.exports = (url = '') => { if (url.startsWith(prefix)) { - return url.replace(prefix, '/proxy/') + return url.replace(prefix, `${config.site}/proxy/`) } return url } From 64296f570ca1bc76f6b4515021848e3b5e1af570 Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 30 Oct 2017 15:38:19 +0800 Subject: [PATCH 057/208] [fix] fix music controller bug --- server/controller/music.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index ccc799d..0e588c2 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -49,7 +49,7 @@ exports.list = async (ctx, next) => { // update cache const data = await exports.updateMusicCache(playListId) - ctx.success(data.list) + ctx.success(data && data.list || []) } } @@ -140,7 +140,7 @@ let lock = false exports.updateMusicCache = async function (playListId = '') { if (lock) { debug.warn('缓存更新中...') - return + return null } lock = true if (!playListId) { @@ -150,7 +150,8 @@ exports.updateMusicCache = async function (playListId = '') { }) if (!option || !option.musicId) { - return debug.warn('歌单ID未配置') + debug.warn('歌单ID未配置') + return null } playListId = option.musicId } From 1eee1a2c009fa30122680e6ab9d6e2d6f0f49bcb Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 30 Oct 2017 15:52:38 +0800 Subject: [PATCH 058/208] [fix] fix redis reconnected bug --- server/plugins/redis.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/server/plugins/redis.js b/server/plugins/redis.js index 4f89083..c8f312c 100644 --- a/server/plugins/redis.js +++ b/server/plugins/redis.js @@ -11,6 +11,7 @@ const config = require('../config') const { getDebug, isType } = require('../util') const debug = getDebug('Redis') let client = null +let connected = false const cache = {} exports.connect = () => { @@ -20,14 +21,17 @@ exports.connect = () => { client = redis.createClient(config.redis) client.on('error', err => { debug.error('连接失败, 错误: ', err.message) - client = null + connected = false + }) + client.on('connect', () => { + debug.success('连接成功') + connected = true }) - client.on('connect', () => debug.success('连接成功')) client.on('reconnecting', () => debug('正在重连中...')) } exports.set = (key = '', value = '') => new Promise((resolve, reject) => { - if (client) { + if (connected) { if (!isType(value, 'String')) { try { value = JSON.stringify(value) @@ -51,7 +55,7 @@ exports.set = (key = '', value = '') => new Promise((resolve, reject) => { exports.get = (key = '') => new Promise((resolve, reject) => { - if (client) { + if (connected) { client.get(key, (err, res) => { if (err) { debug.error('读取【 %s 】失败,错误:%s', key, err.message) From 3174cfa6581e9a22182847c2b72cf7770e92736c Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 30 Oct 2017 18:19:18 +0800 Subject: [PATCH 059/208] [feature] add personal moments api --- README.md | 4 +- server/config/index.js | 1 + server/controller/comment.js | 37 +++++----- server/controller/index.js | 1 + server/controller/moment.js | 131 +++++++++++++++++++++++++++++++++ server/model/schema/comment.js | 2 +- server/model/schema/index.js | 1 + server/model/schema/moment.js | 22 ++++++ server/routes/backend.js | 19 ++++- server/routes/frontend.js | 15 +++- server/util/debug.js | 2 +- server/util/index.js | 2 + server/util/location.js | 23 ++++++ 13 files changed, 237 insertions(+), 23 deletions(-) create mode 100644 server/controller/moment.js create mode 100644 server/model/schema/moment.js create mode 100644 server/util/location.js diff --git a/README.md b/README.md index b64d056..74f5d97 100644 --- a/README.md +++ b/README.md @@ -92,12 +92,14 @@ jooger.me-server * ~~垃圾评论过滤 [akismet](https://github.com/chrisfosterelli/akismet-api)~~ (2017.10.29) -* ~~用户禁言~~ (2017.10.29) +* ~~用户禁言~~ (2017.10.29) * ~~评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer)~~ (2017.10.29) * ~~GC优化~~ (2017.10.30,linux下需要预先安装g++) +* ~~个人动态api~~ (2017.10.30) + * 消息api * 日志api diff --git a/server/config/index.js b/server/config/index.js index eb3e270..c8a9afb 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -29,6 +29,7 @@ const baseConfig = { }, articleLimit: 15, commentLimit: 99, + momentLimit: 10, commentSpamLimit: 3, mongo: { option: { diff --git a/server/controller/comment.js b/server/controller/comment.js index a08fe02..d04dba1 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -6,11 +6,10 @@ 'use strict' -const geoip = require('geoip-lite') const config = require('../config') const { akismet, mailer } = require('../plugins') const { CommentModel, UserModel, ArticleModel } = require('../model') -const { marked, isObjectId, createObjectId, getDebug } = require('../util') +const { marked, isObjectId, createObjectId, getDebug, getLocation } = require('../util') const debug = getDebug('Comment') const isProd = process.env.NODE_ENV === 'development' @@ -254,15 +253,7 @@ exports.create = async (ctx, next) => { comment.sticky = sticky } - // 获取ip - const ip = (req.headers['x-forwarded-for'] || - req.headers['x-real-ip'] || - req.connection.remoteAddress || - req.socket.remoteAddress || - req.connection.socket.remoteAddress || - req.ip || - req.ips[0]).replace('::ffff:', '') - const location = geoip.lookup(ip) + const { ip, location } = getLocation(req) comment.meta = {} comment.meta.location = location || null comment.meta.ip = ip @@ -332,6 +323,7 @@ exports.create = async (ctx, next) => { if (type === 0) { updateArticleCommentCount([comment.article]) } + // 发送邮件通知站主和被评论者 sendEmailToAdminAndUser(data, permalink) } else { ctx.fail() @@ -465,8 +457,8 @@ function getPermalink (comment = {}) { switch (type) { case 0: return `${config.site}/blog/article/${article}` - break - // TODO: 其他页面或组件的permalink + case 1: + return `${config.site}/guestbook` default: return '' break @@ -479,8 +471,10 @@ function getCommentType (type) { case 0: return '博客文章评论' break + case 1: + return '个人站点留言' default: - return '其他评论' + return '评论' break } } @@ -546,19 +540,26 @@ async function updateArticleCommentCount (articleIds = []) { // 发送邮件 async function sendEmailToAdminAndUser (comment, permalink) { const { type, article } = comment - let adminTitle = '博客有新的留言' - if (type == 0) { + let adminTitle = '位置的评论' + let adminType = '评论' + if (type === 0) { // 文章评论 const at = await ArticleModel.findById(article).exec() if (at && at._id) { adminTitle = `博客文章 [${at.title}] 有了新的评论` } + adminType = '评论' + } else if (type === 1) { + // 站内留言 + adminTitle = `个人站点有新的留言` + adminType = '留言' } + // 发送给管理员邮箱config.email mailer.send({ subject: adminTitle, - text: `来自 ${comment.author.github.name} 的${type == 0 ? '评论' : '留言'}:${comment.content}`, - html: `

来自 ${comment.author.github.name} 的${type == 0 ? '评论' : '留言'} [ 点击查看 ]:${comment.renderedContent}

` + text: `来自 ${comment.author.github.name} 的${adminType}:${comment.content}`, + html: `

来自 ${comment.author.github.name} 的${adminType} [ 点击查看 ]:${comment.renderedContent}

` }, true) // 发送给被评论者 diff --git a/server/controller/index.js b/server/controller/index.js index fe92693..99deb1d 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -14,4 +14,5 @@ exports.music = require('./music') exports.option = require('./option') exports.user = require('./user') exports.auth = require('./auth') +exports.moment = require('./moment') exports.statistics = require('./statistics') diff --git a/server/controller/moment.js b/server/controller/moment.js new file mode 100644 index 0000000..cf9d2d7 --- /dev/null +++ b/server/controller/moment.js @@ -0,0 +1,131 @@ +/** + * @desc Moment controller + * @author Jooger + * @date 30 Oct 2017 + */ + +'use strict' + +const config = require('../config') +const { MomentModel } = require('../model') +const { getDebug, getLocation } = require('../util') +const debug = getDebug('Moment') + +exports.list = async (ctx, next) => { + const pageSize = ctx.validateQuery('per_page').defaultTo(config.momentLimit).toInt().gt(0, '每页数量必须大于0').val() + const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() + const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], '个人动态状态参数无效').val() + const keyword = ctx.validateQuery('keyword').optional().toString().val() + + const query = {} + const options = { + page, + limit: pageSize, + sort: { createdAt: -1 } + } + + if (!ctx._isAuthenticated) { + query.state = 1 + } else { + if (state !== undefined) { + query.state = state + } + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { content: keywordReg } + ] + } + } + + const moments = await MomentModel.paginate(query, options).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (moments) { + ctx.success({ + list: moments.docs, + pagination: { + total: moments.total, + current_page: moments.page > moments.pages ? moments.pages : moments.page, + total_page: moments.pages, + per_page: moments.limit + } + }) + } else { + ctx.fail(-1) + } +} + +exports.create = async (ctx, next) => { + const content = ctx.validateBody('content') + .required('内容参数必填') + .notEmpty() + .isString('内容参数必须是字符串类型') + .val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '个人动态状态参数无效').val() + const req = ctx.req + const moment = {} + const { ip, location } = getLocation(req) + + if (state !== undefined) { + moment.state = state + } + moment.location = { ip, ...location } + moment.content = content + + const data = await new MomentModel(moment).save().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.update = async (ctx, next) => { + const id = ctx.validateParam('id').required('个人动态ID参数无效').toString().isObjectId('个人动态ID参数无效').val() + const content = ctx.validateBody('content').optional().isString('内容参数必须是字符串类型').val() + const state = ctx.validateBody('state').optional().toInt().isIn([-2, 0, 1, 2], '个人动态状态参数无效').val() + const req = ctx.req + const moment = {} + + if (state !== undefined) { + moment.state = state + } + content && (moment.content = content) + + const data = await MomentModel.findByIdAndUpdate(id, moment, { + new: true + }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } +} + +exports.delete = async (ctx, next) => { + const id = ctx.validateParam('id').required('个人动态ID参数无效').toString().isObjectId('个人动态ID参数无效').val() + const data = await MomentModel.remove({ _id: id }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data && data.result && data.result.ok) { + ctx.success() + } else { + ctx.fail() + } +} + + diff --git a/server/model/schema/comment.js b/server/model/schema/comment.js index ecb2eb8..d09a61e 100644 --- a/server/model/schema/comment.js +++ b/server/model/schema/comment.js @@ -20,7 +20,7 @@ const commentSchema = new mongoose.Schema({ author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, ups: { type: Number, default: 0 }, // 点赞数 sticky: { type: Number, default: 0 }, // 是否置顶 0 否 | 1 是 - type: { type: Number, default: 0 }, // 类型 0 文章评论 | 1 其他(保留) + type: { type: Number, default: 0 }, // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) meta: { ip: String, // 用户IP location: Object, // IP所在地 diff --git a/server/model/schema/index.js b/server/model/schema/index.js index eea8ec1..79de1e7 100644 --- a/server/model/schema/index.js +++ b/server/model/schema/index.js @@ -12,3 +12,4 @@ exports.category = require('./category') exports.tag = require('./tag') exports.user = require('./user') exports.option = require('./option') +exports.moment = require('./moment') diff --git a/server/model/schema/moment.js b/server/model/schema/moment.js new file mode 100644 index 0000000..51cb1ac --- /dev/null +++ b/server/model/schema/moment.js @@ -0,0 +1,22 @@ +/** + * @desc 个人动态 Model + * @author Jooger + * @date 30 Oct 2017 + */ + +'use strict' + +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate') + +const momentSchema = new mongoose.Schema({ + content: { type: String, required: true, validate: /\S+/ }, + location: { type: Object, required: true }, + state: { type: Number, default: 1 }, // 状态 0 未发布 | 1 发布 + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now } +}) + +momentSchema.plugin(mongoosePaginate) + +module.exports = momentSchema \ No newline at end of file diff --git a/server/routes/backend.js b/server/routes/backend.js index 927fa78..b562c0f 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -7,7 +7,18 @@ 'use strict' const router = require('koa-router')() -const { article, category, tag, comment, option, user, auth, music, statistics } = require('../controller') +const { + article, + category, + tag, + comment, + option, + user, + auth, + music, + statistics, + moment +} = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() @@ -62,6 +73,12 @@ router.get('/auth/local/logout', isAuthenticated, auth.logout) router.post('/auth/local/login', auth.localLogin) router.get('/auth/info', isAuthenticated, auth.info) +// Moment +router.get('/moments', isAuthenticated, moment.list) +router.post('/moments', isAuthenticated, moment.create) +router.patch('/moments/:id', isAuthenticated, moment.update) +router.delete('/moments/:id', isAuthenticated, moment.delete) + // Statistics // TODO: router.get('/statistics', isAuthenticated, statistics.data) diff --git a/server/routes/frontend.js b/server/routes/frontend.js index ad85de1..f2c3d16 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -7,7 +7,17 @@ 'use strict' const router = require('koa-router')() -const { article, category, tag, comment, music, option, user, auth } = require('../controller') +const { + article, + category, + tag, + comment, + music, + option, + user, + auth, + moment +} = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() const snsAuth = authenticate.snsAuth @@ -50,4 +60,7 @@ router.get('/auth/logout', isAuthenticated, auth.logout) router.get('/auth/github/login', snsAuth('github')) .get('/callback', auth.githubLogin) +// Moment +router.get('/moments', moment.list) + module.exports = router diff --git a/server/util/debug.js b/server/util/debug.js index 077376d..983d266 100644 --- a/server/util/debug.js +++ b/server/util/debug.js @@ -16,7 +16,7 @@ const levelMap = { }, info: { level: 6, - emoji: '🤗' + emoji: '⚡️' }, warn: { level: 3, diff --git a/server/util/index.js b/server/util/index.js index a2722bf..00da0e0 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -19,6 +19,8 @@ exports.encrypt = require('./encrypt') exports.proxy = require('./proxy') +exports.getLocation = require('./location') + exports.noop = function () {} exports.isType = (obj = {}, type = 'Object') => { diff --git a/server/util/location.js b/server/util/location.js new file mode 100644 index 0000000..929c44e --- /dev/null +++ b/server/util/location.js @@ -0,0 +1,23 @@ +/** + * @desc 获取ip和location + * @author Jooger + * @date 30 Oct 2017 + */ + +'use strict' + +const geoip = require('geoip-lite') + +module.exports = (req = {}) => { + const ip = (req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + req.connection.socket.remoteAddress || + req.ip || + req.ips[0] || '').replace('::ffff:', '') + return { + ip, + location: geoip.lookup(ip) || {} + } +} From cfd6e53d93a5a8f05522c896337ae588a13e5c9f Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 31 Oct 2017 17:38:54 +0800 Subject: [PATCH 060/208] [update] update authenticate middleware, add sns auth --- server/config/index.js | 5 +- server/controller/auth.js | 2 +- server/controller/music.js | 11 +++-- server/controller/user.js | 18 ++++--- server/middleware/authenticate.js | 82 ++++++++++++++++++++++++++++--- server/model/schema/user.js | 3 +- server/routes/frontend.js | 6 +-- server/service/github-passport.js | 2 +- server/service/netease-music.js | 2 +- 9 files changed, 106 insertions(+), 25 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index c8a9afb..925ff78 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -52,7 +52,8 @@ const baseConfig = { signed: false }, userCookieKey: 'jooger.me.userid', - secrets: `${packageInfo.name} ${packageInfo.version}`, + secrets: `${packageInfo.name}-secrets`, + // 初始化管理员,默认github账户名 defaultName: 'jo0ger', defaultPassword: 'admin_jooger', // 允许请求的域名 @@ -65,6 +66,8 @@ const baseConfig = { }, sns: { github: { + // 登陆后的token的cookie名,每个第三方登录方式必备项 + key: 'jooger.me.github.token', clientID: process.env.githubClientID || 'github client id', clientSecret: process.env.githubClientSecret || 'github client secret', callbackURL: 'github oauth callback url' diff --git a/server/controller/auth.js b/server/controller/auth.js index 6a40b5b..945e1ac 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -106,7 +106,7 @@ exports.githubLogin = async (ctx, next) => { id: user._id, name: user.name }) - ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) + ctx.cookies.set(config.sns.github.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) ctx.cookies.set(config.auth.userCookieKey, user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) debugGithub('Github权限验证回调处理成功') diff --git a/server/controller/music.js b/server/controller/music.js index 0e588c2..865560f 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -131,7 +131,7 @@ function fetchSonglist (playListId) { }) }).catch(err => { debug.error('歌单列表获取失败,错误:', err.message) - return [] + return null }) } @@ -140,7 +140,7 @@ let lock = false exports.updateMusicCache = async function (playListId = '') { if (lock) { debug.warn('缓存更新中...') - return null + return redis.get(cacheKey) || null } lock = true if (!playListId) { @@ -151,12 +151,17 @@ exports.updateMusicCache = async function (playListId = '') { if (!option || !option.musicId) { debug.warn('歌单ID未配置') - return null + lock = false + return redis.get(cacheKey) || null } playListId = option.musicId } const data = await fetchSonglist(playListId) + if (!data) { + lock = false + return redis.get(cacheKey) || null + } const set = { id: playListId, list: data diff --git a/server/controller/user.js b/server/controller/user.js index 251b9aa..4ddc8bc 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -35,14 +35,13 @@ exports.list = async (ctx, next) => { exports.item = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - - let select = '-password' - - if (!ctx._isAuthenticated) { - select += ' -role -createdAt -updatedAt' + let select = '-password -role -createdAt -updatedAt' + + if (ctx._isAuthenticated) { + select = '-password' } - const data = await UserModel.findById(id) + let data = await UserModel.findById(id) .select(select) .exec() .catch(err => { @@ -51,6 +50,13 @@ exports.item = async (ctx, next) => { }) if (data) { + if (ctx._isSnsAuthenticated && id === ctx._user._id) { + // 如果前台已登录而且查询的是本人,返回token + data = { + info: data, + token: ctx.session._snsToken + } + } ctx.success(data) } else { ctx.fail() diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index 87b19c8..4d7882e 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -13,7 +13,9 @@ const config = require('../config') const { UserModel } = require('../model') const debug = require('../util').getDebug('Auth') const isProd = process.env.NODE_ENV === 'production' +const redirectReg = /auth\/github\/login(.*?)/ +// 验证本地登录token function verifyToken () { return async (ctx, next) => { ctx.session._verify = false @@ -24,7 +26,10 @@ function verifyToken () { try { decodedToken = await jwt.verify(token, config.auth.secrets) } catch (err) { - debug.error('Token校验出错,错误:', err.message) + debug.error('本地登录Token校验出错,错误:', err.message) + if (redirectReg.test(ctx.originalUrl)) { + return ctx.redirect(ctx.query.redirectUrl || config.site) + } return ctx.fail(401) } @@ -32,13 +37,47 @@ function verifyToken () { // 已校验权限 ctx.session._verify = true ctx.session._token = token - debug.success('Token校验成功') + debug.success('本地登录Token校验成功') } } await next() } } +// 验证第三方登录token +function vertifySnsToken (name = '') { + return async (ctx, next) => { + if (ctx.session._snsVerify) { + await next() + } + ctx.session._snsVerify = false + if (config.sns[name]) { + const token = ctx.cookies.get(config.sns[name].key) + if (token) { + let decodedToken = null + try { + decodedToken = await jwt.verify(token, config.auth.secrets) + } catch (err) { + debug.error('【%s】第三方登录Token校验出错,错误:%s', name, err.message) + if (redirectReg.test(ctx.originalUrl)) { + return ctx.redirect(ctx.query.redirectUrl || config.site) + } + return ctx.fail(401) + } + + if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { + // 已校验权限 + ctx.session._snsVerify = true + ctx.session._snsToken = token + debug.success('【%s】第三方登录Token校验成功', name) + } + } + } + await next() + } +} + +// 本地登录验证 exports.isAuthenticated = () => { return compose([ verifyToken(), @@ -63,13 +102,41 @@ exports.isAuthenticated = () => { ]) } +// 第三方登录验证 +exports.isSnsAuthenticated = () => { + return compose( + Object.keys(config.sns).map(name => { + return vertifySnsToken(name) + }).concat([ + async (ctx, next) => { + if (!ctx.session._snsVerify) { + return ctx.fail(401) + } + + const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) + const user = await UserModel.findById(userId).exec().catch(err => { + debug.error('用户查找失败, 错误:', err.message) + ctx.log.error(err.message) + return null + }) + if (!user) { + return ctx.fail(401, '用户不存在') + } + ctx._user = user.toObject() + ctx._isSnsAuthenticated = true + await next() + } + ])) +} + +// 单个第三方登录验证 exports.snsAuth = (name = '') => { return compose([ - verifyToken(), + vertifySnsToken(name), async (ctx, next) => { // 如果已经登录 const redirectUrl = ctx.query.redirectUrl || config.site - if (ctx.session._verify) { + if (ctx.session._snsVerify) { debug.info('您已经登录, 重定向中...') return ctx.redirect(redirectUrl) } @@ -83,10 +150,11 @@ exports.snsAuth = (name = '') => { ]) } -exports.snsLogout = () => compose([ - verifyToken(), +// 单个第三方登录退出 +exports.snsLogout = (name = '') => compose([ + vertifySnsToken(name), async (ctx, next) => { - if (!ctx.session._verify) { + if (!ctx.session._snsVerify) { return ctx.fail(-1, '请您先登录') } await next() diff --git a/server/model/schema/user.js b/server/model/schema/user.js index 777c843..9870ea3 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -28,8 +28,7 @@ const userSchema = new mongoose.Schema({ email: { type: String, default: '' }, login: { type: String, default: '' }, name: { type: String, default: '' }, - blog: { type: String, default: '' }, - token: { type: String, default: '' } + blog: { type: String, default: '' } } }) diff --git a/server/routes/frontend.js b/server/routes/frontend.js index f2c3d16..c2c6a2d 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -19,7 +19,7 @@ const { moment } = require('../controller') const { authenticate } = require('../middleware') -const isAuthenticated = authenticate.isAuthenticated() +const isSnsAuthenticated = authenticate.isSnsAuthenticated() const snsAuth = authenticate.snsAuth const snsLogout = authenticate.snsLogout() @@ -53,10 +53,10 @@ router.get('/options', option.data) // User router.get('/users/me', user.me) -router.get('/users/:id', user.item) +router.get('/users/:id', isSnsAuthenticated, user.item) // Auth -router.get('/auth/logout', isAuthenticated, auth.logout) +router.get('/auth/logout', isSnsAuthenticated, auth.logout) router.get('/auth/github/login', snsAuth('github')) .get('/callback', auth.githubLogin) diff --git a/server/service/github-passport.js b/server/service/github-passport.js index ed357c4..72f903c 100644 --- a/server/service/github-passport.js +++ b/server/service/github-passport.js @@ -55,7 +55,7 @@ exports.init = (UserModel, config) => { role: 1 } - newUser.github.token = accessToken + // newUser.github.token = accessToken const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { debug.error('本地用户查找失败, 错误:', err.message) diff --git a/server/service/netease-music.js b/server/service/netease-music.js index ba59d8f..1374a10 100644 --- a/server/service/netease-music.js +++ b/server/service/netease-music.js @@ -1,5 +1,5 @@ /** - * @desc 网易云音乐 TEST + * @desc 网易云音乐 TEST (暂未使用) * @author Jooger * @date 30 Sep 2017 */ From 2be50a1bf98dd9214ccbae506817643dde6a9b5e Mon Sep 17 00:00:00 2001 From: Jooger Date: Fri, 3 Nov 2017 18:47:24 +0800 Subject: [PATCH 061/208] [update] remove sns auth login, upgrade to 1.3.0 --- package.json | 4 +- server/app.js | 18 +++- server/config/development.js | 3 + server/controller/auth.js | 130 +++++++++++++++++++------ server/controller/comment.js | 44 +++++++-- server/middleware/authenticate.js | 157 ++++++++++++++---------------- server/model/schema/index.js | 1 + server/model/schema/log.js | 10 ++ server/model/schema/user.js | 2 +- server/routes/frontend.js | 19 ++-- server/routes/index.js | 1 + server/service/github-passport.js | 14 ++- server/service/github-token.js | 35 +++++++ server/service/github-userinfo.js | 17 +++- server/service/index.js | 8 +- 15 files changed, 318 insertions(+), 145 deletions(-) create mode 100644 server/service/github-token.js diff --git a/package.json b/package.json index 59ad56d..52f4d3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jooger.me-server", - "version": "1.2.0", + "version": "1.3.0", "private": true, "description": "🔥 My blog's api server build by koa2 and mongoose", "scripts": { @@ -14,7 +14,7 @@ }, "author": "Jooger", "site": "https://jooger.me", - "email": "zzy1198258955@163.com", + "email": "iamjooger@gmail.com", "repository": { "type": "https", "url": "https://github.com/jo0ger/jooger.me-server.git" diff --git a/server/app.js b/server/app.js index da6d7d4..edbb1ea 100644 --- a/server/app.js +++ b/server/app.js @@ -16,10 +16,13 @@ const passport = require('koa-passport') const compress = require('koa-compress') const bodyparser = require('koa-bodyparser') const koaBunyanLogger = require('koa-bunyan-logger') + +const packageInfo = require('../package.json') const middlewares = require('./middleware') const config = require('./config') const { mongo, redis, akismet, validation, mailer, gc } = require('./plugins') const { crontab } = require('./service') +const isProd = process.env.NODE_ENV === 'production' const app = new Koa() @@ -43,7 +46,12 @@ app.use(bodyparser({ })) app.use(json()) app.use(logger()) -app.use(koaBunyanLogger()) +app.use(koaBunyanLogger({ + name: packageInfo.name, + level: 'debug' +})) +// app.use(koaBunyanLogger.requestIdContext()) +// app.use(koaBunyanLogger.requestLogger()) app.use(bouncer.middleware()) app.use(middlewares.response) app.use(middlewares.error) @@ -66,7 +74,11 @@ mailer.start() // crontab crontab.start() -// v8 gc -gc.start() +if (isProd) { + // v8 gc + gc.start() +} + +app.on('error', function () {}) module.exports = app diff --git a/server/config/development.js b/server/config/development.js index 99f9c42..914343b 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -14,6 +14,9 @@ module.exports = { }, sns: { github: { + // 测试用的ID和Secret + clientID: '5b4d4a7945347d0fd2e2', + clientSecret: '8771bd9ae52749cc15b0c9e2c6cb4ecd7f39d9da', callbackURL: 'http://127.0.0.1:3001/auth/github/login/callback' } } diff --git a/server/controller/auth.js b/server/controller/auth.js index 945e1ac..3e121bf 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -10,12 +10,11 @@ const jwt = require('jsonwebtoken') const passport = require('koa-passport') const config = require('../config') const { UserModel } = require('../model') -const { bhash, bcompare, getDebug, signToken } = require('../util') +const { bhash, bcompare, getDebug, signToken, proxy, randomString } = require('../util') const debug = getDebug('Auth') const debugGithub = getDebug('Github:Auth') -const { githubPassport } = require('../service') - -githubPassport.init(UserModel, config) +const { getGithubToken, getGithubAuthUserInfo } = require('../service') +const isProd = process.env.NODE_ENV === 'production' exports.localLogin = async (ctx, next) => { const name = ctx.validateBody('name') @@ -71,17 +70,22 @@ exports.logout = async (ctx, next) => { exports.info = async (ctx, next) => { const adminId = ctx._user._id - if (!adminId) { + if (!adminId && !ctx._isSnsAuthenticated && !ctx._isAuthenticated) { return ctx.fail(401) } + let data = null + if (ctx._isSnsAuthenticated) { + // TODO: 第三方信息获取 + } else if (ctx._isAuthenticated) { + data = await UserModel.findById(adminId) + .select('-password') + .exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + } - const data = await UserModel.findById(adminId) - .select('-password') - .exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) if (data) { ctx.success({ info: data, @@ -93,24 +97,90 @@ exports.info = async (ctx, next) => { } // github login -exports.githubLogin = async (ctx, next) => { - await passport.authenticate('github', { - session: false - }, (err, user) => { - debugGithub('Github权限验证回调处理开始') - const redirectUrl = ctx.session.passport.redirectUrl - const cookieDomain = config.auth.session.domain || null - - const { session } = config.auth - const token = signToken({ - id: user._id, - name: user.name +// exports.githubLogin = async (ctx, next) => { +// await passport.authenticate('github', { +// session: false +// }, (err, user) => { +// debugGithub('Github权限验证回调处理开始') +// const redirectUrl = ctx.session.passport.redirectUrl + +// const { session } = config.auth +// const opt = { signed: false, maxAge: session.maxAge, httpOnly: false } +// opt.domain = session.domain || null +// ctx.cookies.set(config.sns.github.key, user.token, opt) +// ctx.cookies.set(config.auth.userCookieKey, user._id, opt) + +// debugGithub.success('Github权限验证回调处理成功, 用户ID:%s,用户名:%s', user._id, user.name) +// return ctx.redirect(redirectUrl) +// })(ctx) +// } + +exports.fetchGithubToken = async (ctx, next) => { + const code = ctx.validateQuery('code').required('缺少code参数').toString().val() + const token = await getGithubToken(code) + if (token) { + ctx.success(token) + } else { + ctx.fail('Token获取失败') + } +} + +exports.fetchGithubUser = async (ctx, next) => { + const accessToken = ctx.validateQuery('access_token').required('缺少access_token参数').toString().val() + const data = await getGithubAuthUserInfo(accessToken) + if (!data) { + return ctx.fail('用户信息获取失败') + } + const user = await createLocalUserFromGithub(data) + if (user) { + ctx.success(user) + } else { + ctx.fail('用户信息获取失败') + } +} + +async function createLocalUserFromGithub (githubUser) { + const user = await UserModel.findOne({ + 'github.id': githubUser.id + }).catch(err => { + debug.error('本地用户查找失败, 错误:', err.message) + return null + }) + + if (user) { + const userData = { + name: githubUser.username || githubUser.login, + avatar: proxy(githubUser.avatar_url), + slogan: githubUser.bio, + github: githubUser, + role: user.role + } + const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData) + .select('-password -role -createdAt -updatedAt') + .exec().catch(err => { + debug.error('本地用户更新失败, 错误:', err.message) + }) || user + + return updatedUser.toObject() + } else { + const newUser = { + name: githubUser.username || githubUser.login, + avatar: proxy(githubUser.avatar_url), + slogan: githubUser.bio, + github: githubUser, + role: 1 + } + + const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { + debug.error('本地用户查找失败, 错误:', err.message) + return true }) - ctx.cookies.set(config.sns.github.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - ctx.cookies.set(config.auth.userCookieKey, user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - debugGithub('Github权限验证回调处理成功') - debugGithub.success('Github权限验证回调处理成功, 用户ID:%s,用户名:%s', user._id, user.name) - return ctx.redirect(redirectUrl) - })(ctx) + if (checkUser) { + newUser.name += '-' + randomString() + } + + const data = await new UserModel(newUser).save().catch(err => debug.error('本地用户创建失败, 错误:', err.message)) + return data && data.toObject() || null + } } diff --git a/server/controller/comment.js b/server/controller/comment.js index d04dba1..a40ba93 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -21,6 +21,7 @@ exports.list = async (ctx, next) => { const author = ctx.validateQuery('author').optional().toString().isObjectId('用户ID参数无效').val() const article = ctx.validateQuery('article').optional().toString().isObjectId('文章ID参数无效').val() const keyword = ctx.validateQuery('keyword').optional().toString().val() + const parent = ctx.validateQuery('parent').optional().toString().isObjectId('父评论ID参数无效').val() // 时间区间查询仅后台可用,且依赖于createdAt const startDate = ctx.validateQuery('start_date').optional().toString().val() const endDate = ctx.validateQuery('end_date').optional().toString().val() @@ -39,7 +40,7 @@ exports.list = async (ctx, next) => { populate: [ { path: 'author', - select: !ctx._isAuthenticated ? 'github' : '' + select: !ctx._isAuthenticated ? 'github avatar name' : '' }, { path: 'parent', @@ -53,6 +54,10 @@ exports.list = async (ctx, next) => { select: 'author meta sticky ups', match: { state: 1 + }, + populate: { + path: 'author', + select: 'avatar github name' } } ] @@ -109,6 +114,20 @@ exports.list = async (ctx, next) => { } } + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } + + if (parent) { + // 获取子评论 + query.parent = parent + } else { + // 获取父评论 + query.parent = { $exists: false } + } + // 未通过权限校验(前台获取评论列表) if (!ctx._isAuthenticated) { // 将评论状态重置为1 @@ -117,12 +136,6 @@ exports.list = async (ctx, next) => { // 评论列表不需要content和state options.select = '-content -state -updatedAt -spam -type' } else { - // 排序 - if (sortBy && order) { - options.sort = {} - options.sort[sortBy] = order - } - // 起始日期 if (startDate) { const $gte = new Date(startDate) @@ -146,8 +159,23 @@ exports.list = async (ctx, next) => { }) if (comments) { + const data = [] + // 查询子评论数量 + await Promise.all(comments.docs.map(doc => { + doc = doc.toObject() + doc.subCount = 0 + data.push(doc) + return CommentModel.count({ parent: doc._id }).exec() + .then(count => { + doc.subCount = count + }) + .catch(err => { + ctx.log.error(err) + doc.subCount = 0 + }) + })) ctx.success({ - list: comments.docs, + list: data, pagination: { total: comments.total, current_page: comments.page > comments.pages ? comments.pages : comments.page, diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index 4d7882e..ce46921 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -45,37 +45,26 @@ function verifyToken () { } // 验证第三方登录token -function vertifySnsToken (name = '') { - return async (ctx, next) => { - if (ctx.session._snsVerify) { - await next() - } - ctx.session._snsVerify = false - if (config.sns[name]) { - const token = ctx.cookies.get(config.sns[name].key) - if (token) { - let decodedToken = null - try { - decodedToken = await jwt.verify(token, config.auth.secrets) - } catch (err) { - debug.error('【%s】第三方登录Token校验出错,错误:%s', name, err.message) - if (redirectReg.test(ctx.originalUrl)) { - return ctx.redirect(ctx.query.redirectUrl || config.site) - } - return ctx.fail(401) - } - - if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { - // 已校验权限 - ctx.session._snsVerify = true - ctx.session._snsToken = token - debug.success('【%s】第三方登录Token校验成功', name) - } - } - } - await next() - } -} +// function vertifySnsToken (name = '') { +// return async (ctx, next) => { +// if (ctx.session._snsVerify) { +// await next() +// } +// ctx.session._snsVerify = false +// if (config.sns[name]) { +// const token = ctx.cookies.get(config.sns[name].key, { signed: false }) + +// console.log(token, config.sns[name].key) +// if (token) { +// ctx.session._snsVerify = true +// ctx.session._snsToken = token +// ctx.session._snsName = name +// debug.success('【%s】第三方登录Token校验成功', name) +// } +// } +// await next() +// } +// } // 本地登录验证 exports.isAuthenticated = () => { @@ -103,60 +92,60 @@ exports.isAuthenticated = () => { } // 第三方登录验证 -exports.isSnsAuthenticated = () => { - return compose( - Object.keys(config.sns).map(name => { - return vertifySnsToken(name) - }).concat([ - async (ctx, next) => { - if (!ctx.session._snsVerify) { - return ctx.fail(401) - } +// exports.isSnsAuthenticated = () => { +// return compose( +// Object.keys(config.sns).map(name => { +// return vertifySnsToken(name) +// }).concat([ +// async (ctx, next) => { +// if (!ctx.session._snsVerify) { +// return ctx.fail(401) +// } - const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) - const user = await UserModel.findById(userId).exec().catch(err => { - debug.error('用户查找失败, 错误:', err.message) - ctx.log.error(err.message) - return null - }) - if (!user) { - return ctx.fail(401, '用户不存在') - } - ctx._user = user.toObject() - ctx._isSnsAuthenticated = true - await next() - } - ])) -} +// const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) +// const user = await UserModel.findById(userId).exec().catch(err => { +// debug.error('用户查找失败, 错误:', err.message) +// ctx.log.error(err.message) +// return null +// }) +// if (!user) { +// return ctx.fail(401, '用户不存在') +// } +// ctx._user = user.toObject() +// ctx._isSnsAuthenticated = true +// await next() +// } +// ])) +// } // 单个第三方登录验证 -exports.snsAuth = (name = '') => { - return compose([ - vertifySnsToken(name), - async (ctx, next) => { - // 如果已经登录 - const redirectUrl = ctx.query.redirectUrl || config.site - if (ctx.session._snsVerify) { - debug.info('您已经登录, 重定向中...') - return ctx.redirect(redirectUrl) - } - ctx.session.passport = { redirectUrl } - await next() - }, - passport.authenticate(name, { - failureRedirect: '/', - session: false - }) - ]) -} +// exports.snsAuth = (name = '') => { +// return compose([ +// vertifySnsToken(name), +// async (ctx, next) => { +// // 如果已经登录 +// const redirectUrl = ctx.query.redirectUrl || config.site +// if (ctx.session._snsVerify) { +// debug.info('您已经登录, 重定向中...') +// return ctx.redirect(redirectUrl) +// } +// ctx.session.passport = { redirectUrl } +// await next() +// }, +// passport.authenticate(name, { +// failureRedirect: '/', +// session: false +// }) +// ]) +// } -// 单个第三方登录退出 -exports.snsLogout = (name = '') => compose([ - vertifySnsToken(name), - async (ctx, next) => { - if (!ctx.session._snsVerify) { - return ctx.fail(-1, '请您先登录') - } - await next() - } -]) +// // 单个第三方登录退出 +// exports.snsLogout = (name = '') => compose([ +// vertifySnsToken(name), +// async (ctx, next) => { +// if (!ctx.session._snsVerify) { +// return ctx.fail(-1, '请您先登录') +// } +// await next() +// } +// ]) diff --git a/server/model/schema/index.js b/server/model/schema/index.js index 79de1e7..6c747a4 100644 --- a/server/model/schema/index.js +++ b/server/model/schema/index.js @@ -13,3 +13,4 @@ exports.tag = require('./tag') exports.user = require('./user') exports.option = require('./option') exports.moment = require('./moment') +exports.log = require('./log') diff --git a/server/model/schema/log.js b/server/model/schema/log.js index a8934e2..8d58c9c 100644 --- a/server/model/schema/log.js +++ b/server/model/schema/log.js @@ -6,3 +6,13 @@ 'use strict' +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate') + +const logSchema = new mongoose.Schema({ + createdAt: { type: Date, default: Date.now } +}) + +logSchema.plugin(mongoosePaginate) + +module.exports = logSchema diff --git a/server/model/schema/user.js b/server/model/schema/user.js index 9870ea3..bb8bff2 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -28,7 +28,7 @@ const userSchema = new mongoose.Schema({ email: { type: String, default: '' }, login: { type: String, default: '' }, name: { type: String, default: '' }, - blog: { type: String, default: '' } + blog: { type: String, default: '' }, } }) diff --git a/server/routes/frontend.js b/server/routes/frontend.js index c2c6a2d..16bc418 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -18,10 +18,10 @@ const { auth, moment } = require('../controller') -const { authenticate } = require('../middleware') -const isSnsAuthenticated = authenticate.isSnsAuthenticated() -const snsAuth = authenticate.snsAuth -const snsLogout = authenticate.snsLogout() +// const { authenticate } = require('../middleware') +// const isSnsAuthenticated = authenticate.isSnsAuthenticated() +// const snsAuth = authenticate.snsAuth +// const snsLogout = authenticate.snsLogout() // Article router.get('/articles', article.list) @@ -53,12 +53,15 @@ router.get('/options', option.data) // User router.get('/users/me', user.me) -router.get('/users/:id', isSnsAuthenticated, user.item) +// router.get('/users/:id', isSnsAuthenticated, user.item) // Auth -router.get('/auth/logout', isSnsAuthenticated, auth.logout) -router.get('/auth/github/login', snsAuth('github')) - .get('/callback', auth.githubLogin) +// router.get('/auth/info', isSnsAuthenticated, auth.info) +// router.get('/auth/logout', isSnsAuthenticated, auth.logout) +// router.get('/auth/github/login', snsAuth('github')) +// .get('/callback', auth.githubLogin) +router.get('/auth/github/token', auth.fetchGithubToken) +router.get('/auth/github/user', auth.fetchGithubUser) // Moment router.get('/moments', moment.list) diff --git a/server/routes/index.js b/server/routes/index.js index 25cf782..791d770 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -16,6 +16,7 @@ module.exports = app => { router.use('*', header) router.get('/', async (ctx, next) => { + ctx.log.error('Got a request from %s for %s', ctx.request.ip, ctx.path) ctx.body = { name: config.name, version: config.version, diff --git a/server/service/github-passport.js b/server/service/github-passport.js index 72f903c..30a1425 100644 --- a/server/service/github-passport.js +++ b/server/service/github-passport.js @@ -37,14 +37,15 @@ exports.init = (UserModel, config) => { github: profile._json, role: user.role } - - userData.github.token = accessToken - + // userData.github.token = accessToken const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData).exec().catch(err => { debug.error('本地用户更新失败, 错误:', err.message) }) || user - return end(null, updatedUser) + return end(null, { + ...updatedUser.toObject(), + token: accessToken + }) } const newUser = { @@ -70,7 +71,10 @@ exports.init = (UserModel, config) => { debug.error('本地用户创建失败, 错误:', err.message) }) - return end(null, data) + return end(null, { + ...data.toObject(), + token: access + }) } catch (err) { debug.error('Github权限验证失败,错误:', err) return end(err) diff --git a/server/service/github-token.js b/server/service/github-token.js new file mode 100644 index 0000000..a11ffbe --- /dev/null +++ b/server/service/github-token.js @@ -0,0 +1,35 @@ +/** + * @desc Github access_token + * @author Jooger + * @date 2 Nov 2017 + */ + +'use strict' + +const axios = require('axios') +const { getDebug } = require('../util') +const config = require('../config') +const { clientID, clientSecret } = config.sns.github +const debug = getDebug('Github:Token') + +module.exports = async (code) => { + const data = await axios.post('https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token', { + client_id: clientID, + client_secret: clientSecret, + code + }, { + headers: { + Accept: 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .catch(err => { + debug.error('Github Token获取失败,错误:', err.message) + return null + }) + + if (data && data.data.access_token) { + return data.data + } + return null +} diff --git a/server/service/github-userinfo.js b/server/service/github-userinfo.js index 9a6b3f3..5999491 100644 --- a/server/service/github-userinfo.js +++ b/server/service/github-userinfo.js @@ -12,7 +12,7 @@ const config = require('../config') const { clientID, clientSecret } = config.sns.github const debug = getDebug('Github:User') -const getGithubUsersInfo = (githubNames = '') => { +exports.getGithubUsersInfo = (githubNames = '') => { if (!githubNames) { return null } else if (typeof githubNames === 'string') { @@ -27,6 +27,10 @@ const getGithubUsersInfo = (githubNames = '') => { client_id: clientID, client_secret: clientSecret } + }, { + headers: { + Accept: 'application/json' + } }).then(res => { if (res && res.status === 200) { debug.success('抓取【 %s 】信息成功', name,) @@ -43,4 +47,13 @@ const getGithubUsersInfo = (githubNames = '') => { return Promise.all(task) } -module.exports = getGithubUsersInfo +exports.getGithubAuthUserInfo = (access_token = '') => { + return axios.get('https://api.github.com/user', { + params: { access_token } + }).then(res => { + return res.data + }).catch(err => { + debug.error('获取用户信息失败,错误:', err.message) + return null + }) +} \ No newline at end of file diff --git a/server/service/index.js b/server/service/index.js index 473f105..cbeafb4 100644 --- a/server/service/index.js +++ b/server/service/index.js @@ -6,7 +6,11 @@ 'use strict' -exports.githubPassport = require('./github-passport') -exports.getGithubUsersInfo = require('./github-userinfo') +const { getGithubUsersInfo, getGithubAuthUserInfo } = require('./github-userinfo') + +// exports.githubPassport = require('./github-passport') +exports.getGithubUsersInfo = getGithubUsersInfo +exports.getGithubAuthUserInfo = getGithubAuthUserInfo +exports.getGithubToken = require('./github-token') exports.fetchNE = require('./netease-music') exports.crontab = require('./crontab') From c5b94bf501158bdddba43091f32fc18d59ea4153 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 4 Nov 2017 19:57:37 +0800 Subject: [PATCH 062/208] [update] update auth api --- server/config/index.js | 1 - server/controller/auth.js | 6 +++--- server/controller/comment.js | 2 +- server/controller/option.js | 2 +- server/controller/user.js | 2 +- server/plugins/mongo.js | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index 925ff78..95ef1ba 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -60,7 +60,6 @@ const baseConfig = { allowedOrigins: [ 'jooger.me', 'www.jooger.me', - 'blog.jooger.me', 'admin.jooger.me' ] }, diff --git a/server/controller/auth.js b/server/controller/auth.js index 3e121bf..af7b6f8 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -146,10 +146,10 @@ async function createLocalUserFromGithub (githubUser) { debug.error('本地用户查找失败, 错误:', err.message) return null }) - + console.log(githubUser) if (user) { const userData = { - name: githubUser.username || githubUser.login, + name: githubUser.name || githubUser.login, avatar: proxy(githubUser.avatar_url), slogan: githubUser.bio, github: githubUser, @@ -164,7 +164,7 @@ async function createLocalUserFromGithub (githubUser) { return updatedUser.toObject() } else { const newUser = { - name: githubUser.username || githubUser.login, + name: githubUser.name || githubUser.login, avatar: proxy(githubUser.avatar_url), slogan: githubUser.bio, github: githubUser, diff --git a/server/controller/comment.js b/server/controller/comment.js index a40ba93..a2e71ba 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -152,7 +152,7 @@ exports.list = async (ctx, next) => { } } } - + const comments = await CommentModel.paginate(query, options).catch(err => { ctx.log.error(err.message) return null diff --git a/server/controller/option.js b/server/controller/option.js index 64a4692..bad824d 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -82,7 +82,7 @@ async function generateLinks (links = []) { if (userInfo) { link.avatar = proxy(userInfo.avatar_url) link.slogan = userInfo.bio - link.site = link.site || userInfo.blog + link.site = link.site || userInfo.blog || userInfo.url } return link }) diff --git a/server/controller/user.js b/server/controller/user.js index 4ddc8bc..6d0334b 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -166,7 +166,7 @@ exports.updateGithubInfo = async () => { email: data.email, login: data.login, name: data.name, - blog: data.blog + blog: data.blog || data.url } } // 非管理员更新其他信息,管理员只更新github信息 diff --git a/server/plugins/mongo.js b/server/plugins/mongo.js index c15f194..b1280a2 100644 --- a/server/plugins/mongo.js +++ b/server/plugins/mongo.js @@ -72,7 +72,7 @@ async function createAdmin () { email: data.email, login: data.login, name: data.name, - blog: data.blog + blog: data.blog || data.url } }) .save() From 0f161bfed5a4350b06a990bd380ec3e8a779f2cf Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 4 Nov 2017 19:57:37 +0800 Subject: [PATCH 063/208] [update] update auth api --- README.md | 5 +++-- server/config/index.js | 1 - server/controller/auth.js | 5 ++--- server/controller/comment.js | 2 +- server/controller/option.js | 2 +- server/controller/user.js | 2 +- server/plugins/mongo.js | 2 +- 7 files changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 74f5d97..e83f38d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -[![GitHub issues](https://img.shields.io/github/issues/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/issues) [![GitHub forks](https://img.shields.io/github/forks/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/network) [![GitHub stars](https://img.shields.io/github/stars/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/stargazers) +[![GitHub issues](https://img.shields.io/github/issues/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/issues) +[![GitHub last commit](https://img.shields.io/github/last-commit/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/commits/master) ## jooger.me-server @@ -43,7 +44,7 @@ $ npm run test ``` jooger.me-server -|____api.md // api文档(带完善) +|____api.md // api文档(待完善) |____bin // 启动目录 |____ecosystem.config.js // pm2启动文件,需要自己手动创建 |____LICENSE // LICENSE(MIT) diff --git a/server/config/index.js b/server/config/index.js index 925ff78..95ef1ba 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -60,7 +60,6 @@ const baseConfig = { allowedOrigins: [ 'jooger.me', 'www.jooger.me', - 'blog.jooger.me', 'admin.jooger.me' ] }, diff --git a/server/controller/auth.js b/server/controller/auth.js index 3e121bf..25b1344 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -146,10 +146,9 @@ async function createLocalUserFromGithub (githubUser) { debug.error('本地用户查找失败, 错误:', err.message) return null }) - if (user) { const userData = { - name: githubUser.username || githubUser.login, + name: githubUser.name || githubUser.login, avatar: proxy(githubUser.avatar_url), slogan: githubUser.bio, github: githubUser, @@ -164,7 +163,7 @@ async function createLocalUserFromGithub (githubUser) { return updatedUser.toObject() } else { const newUser = { - name: githubUser.username || githubUser.login, + name: githubUser.name || githubUser.login, avatar: proxy(githubUser.avatar_url), slogan: githubUser.bio, github: githubUser, diff --git a/server/controller/comment.js b/server/controller/comment.js index a40ba93..a2e71ba 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -152,7 +152,7 @@ exports.list = async (ctx, next) => { } } } - + const comments = await CommentModel.paginate(query, options).catch(err => { ctx.log.error(err.message) return null diff --git a/server/controller/option.js b/server/controller/option.js index 64a4692..bad824d 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -82,7 +82,7 @@ async function generateLinks (links = []) { if (userInfo) { link.avatar = proxy(userInfo.avatar_url) link.slogan = userInfo.bio - link.site = link.site || userInfo.blog + link.site = link.site || userInfo.blog || userInfo.url } return link }) diff --git a/server/controller/user.js b/server/controller/user.js index 4ddc8bc..6d0334b 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -166,7 +166,7 @@ exports.updateGithubInfo = async () => { email: data.email, login: data.login, name: data.name, - blog: data.blog + blog: data.blog || data.url } } // 非管理员更新其他信息,管理员只更新github信息 diff --git a/server/plugins/mongo.js b/server/plugins/mongo.js index c15f194..b1280a2 100644 --- a/server/plugins/mongo.js +++ b/server/plugins/mongo.js @@ -72,7 +72,7 @@ async function createAdmin () { email: data.email, login: data.login, name: data.name, - blog: data.blog + blog: data.blog || data.url } }) .save() From fc4da36852e5243d8e8e817e357624d1b04ece5c Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 4 Nov 2017 23:47:13 +0800 Subject: [PATCH 064/208] [update] update email, update akismet log --- package.json | 2 +- server/plugins/akismet.js | 13 ++++++++----- server/plugins/mailer.js | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 52f4d3c..e92dc01 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "author": "Jooger", "site": "https://jooger.me", - "email": "iamjooger@gmail.com", + "email": "zzy1198258955@163.com", "repository": { "type": "https", "url": "https://github.com/jo0ger/jooger.me-server.git" diff --git a/server/plugins/akismet.js b/server/plugins/akismet.js index e325ee4..e5aad8f 100644 --- a/server/plugins/akismet.js +++ b/server/plugins/akismet.js @@ -36,18 +36,21 @@ class AkismetClient { async verifyKey () { let valid = true + let error = '' if (!isValidKey) { await this.client.verifyKey().then(v => { valid = v if (v) { isValidKey = true } else { - debug.error(`无效的Apikey`) + error = '无效的Apikey' this.client = null } - }).catch(err => debug.error('Apikey验证失败,错误:', err.message)) + }).catch(err => { + error = 'Apikey验证失败,错误:' + err.message + }) } - return { valid, client: this } + return { valid, client: this, error } } // 检测是否是spam @@ -123,13 +126,13 @@ exports.start = async () => { const akismetConfig = config.akismet const { apiKey } = akismetConfig const site = config.site - const { valid, client } = await new AkismetClient(apiKey, site).verifyKey() + const { valid, client, error } = await new AkismetClient(apiKey, site).verifyKey() if (valid) { debug.success('服务启动成功') akismetClient = client } else { - debug.error('服务启动失败') + debug.error('服务启动失败', error ? `,${error}` : '') } } diff --git a/server/plugins/mailer.js b/server/plugins/mailer.js index 1426dd0..d7cf425 100644 --- a/server/plugins/mailer.js +++ b/server/plugins/mailer.js @@ -35,7 +35,7 @@ exports.start = async () => { resolve() } }) - }) + }).catch(() => ({})) } /** From 2d450ca17b251903033cc1ba5e70b5a640f9ff49 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 5 Nov 2017 00:10:04 +0800 Subject: [PATCH 065/208] [update] update comment send mail --- README.md | 2 ++ server/controller/comment.js | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e83f38d..ba95025 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ jooger.me-server * ~~个人动态api~~ (2017.10.30) +* 邮件模板 + * 消息api * 日志api diff --git a/server/controller/comment.js b/server/controller/comment.js index a2e71ba..428d968 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -587,7 +587,7 @@ async function sendEmailToAdminAndUser (comment, permalink) { mailer.send({ subject: adminTitle, text: `来自 ${comment.author.github.name} 的${adminType}:${comment.content}`, - html: `

来自 ${comment.author.github.name} 的${adminType} [ 点击查看 ]:${comment.renderedContent}

` + html: `

来自 ${comment.author.github.name} 的${adminType} [ 点击查看 ]:${comment.renderedContent}

` }, true) // 发送给被评论者 @@ -596,9 +596,9 @@ async function sendEmailToAdminAndUser (comment, permalink) { if (forwardAuthor) { mailer.send({ to: forwardAuthor.github.email, - subject: '你在Jooger的博客的评论有了新的回复', + subject: '你在 Jooger 的博客的评论有了新的回复', text: `来自 ${comment.author.name} 的回复:${comment.content}`, - html: `

来自 ${comment.author.name} 的回复 [ 点击查看 ]:${comment.renderedContent}

` + html: `

来自 ${comment.author.name} 的回复 [ 点击查看 ]:${comment.renderedContent}

` }) } else { debug.warn('给被评论者邮件失败') From 773d02e7176bb78bbdaab69537a4d367f2f28e1c Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 5 Nov 2017 19:02:38 +0800 Subject: [PATCH 066/208] [update] update marked, add rel="noopener" for "a" link --- server/util/marked.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/util/marked.js b/server/util/marked.js index 21f961d..66d7f6b 100644 --- a/server/util/marked.js +++ b/server/util/marked.js @@ -41,7 +41,7 @@ renderer.link = function (href, title, text) { class="${isImage ? 'img-link' : 'link'}" ${isImage && 'onclick=""'} title="${title || ''}" - ${isOrigin ? '' : 'rel="external nofollow"'}>${text} + ${isOrigin ? '' : 'rel="noopener external nofollow"'}>${text} `.replace(/\s+/g, ' ').replace('\n', '') } From 1d5edca9b24a19a0bec47d9c5867a88a6f680a0d Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 7 Nov 2017 15:07:01 +0800 Subject: [PATCH 067/208] [fix] fix user api proxy reference bug --- server/app.js | 3 --- server/controller/user.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/server/app.js b/server/app.js index edbb1ea..0817df8 100644 --- a/server/app.js +++ b/server/app.js @@ -50,8 +50,6 @@ app.use(koaBunyanLogger({ name: packageInfo.name, level: 'debug' })) -// app.use(koaBunyanLogger.requestIdContext()) -// app.use(koaBunyanLogger.requestLogger()) app.use(bouncer.middleware()) app.use(middlewares.response) app.use(middlewares.error) @@ -59,7 +57,6 @@ app.use(middlewares.error) // app.use(middlewares.formidable()) app.use(session(config.auth.session, app)) app.use(passport.initialize()) -// app.use(passport.session()) app.use(compress()) // routes diff --git a/server/controller/user.js b/server/controller/user.js index 6d0334b..00825a5 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -8,7 +8,7 @@ const config = require('../config') const { UserModel } = require('../model') -const { bhash, bcompare, getDebug } = require('../util') +const { bhash, bcompare, getDebug, proxy } = require('../util') const { getGithubUsersInfo } = require('../service') const debug = getDebug('User') From cf08bfdfebc870acf5c283aaf36bfed488db1921 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 8 Nov 2017 00:29:00 +0800 Subject: [PATCH 068/208] [update] There seems to be a problem with this module(simple-netease-cloud-music) --- server/controller/music.js | 3 ++- server/middleware/authenticate.js | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index 865560f..9437a92 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -114,7 +114,8 @@ exports.cover = async (ctx, next) => { // 获取除了歌曲链接和歌词外其他信息 function fetchSonglist (playListId) { - return neteaseMusic.playlist(playListId).then(({ playlist }) => { + // return neteaseMusic.playlist(playListId).then(({ playlist }) => { + return fetchNE('playlist', playListId).then(({ playlist }) => { return !playlist ? [] : playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { return { id, diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index ce46921..091c011 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -54,7 +54,6 @@ function verifyToken () { // if (config.sns[name]) { // const token = ctx.cookies.get(config.sns[name].key, { signed: false }) -// console.log(token, config.sns[name].key) // if (token) { // ctx.session._snsVerify = true // ctx.session._snsToken = token From 5b5a7d778de351ee9bc80b2ced44c4d7462fcb6a Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 8 Nov 2017 12:54:57 +0800 Subject: [PATCH 069/208] [update] update redis expired, add expired time(10 minutes) for music --- server/controller/music.js | 6 ++++-- server/controller/user.js | 9 +-------- server/plugins/redis.js | 5 +++-- server/routes/frontend.js | 9 --------- server/service/crontab.js | 4 ---- 5 files changed, 8 insertions(+), 25 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index 9437a92..a40f13b 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -12,6 +12,7 @@ const { fetchNE } = require('../service') const { OptionModel } = require('../model') const { proxy, getDebug, isType } = require('../util') const { redis } = require('../plugins') +const expired = 60 * 10 // 过期时间10分钟 const isProd = process.env.NODE_ENV === 'production' const debug = getDebug('Music') @@ -167,8 +168,9 @@ exports.updateMusicCache = async function (playListId = '') { id: playListId, list: data } - - redis.set(cacheKey, set).then(() => { + + // 设置10分钟过期 + redis.set(cacheKey, set, expired).then(() => { debug.success('缓存更新成功,歌单ID:', playListId) }).catch(err => { debug.error('缓存更新失败,歌单ID:%s,错误:%s', playListId, err.message) diff --git a/server/controller/user.js b/server/controller/user.js index 00825a5..ede7cd1 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -50,13 +50,6 @@ exports.item = async (ctx, next) => { }) if (data) { - if (ctx._isSnsAuthenticated && id === ctx._user._id) { - // 如果前台已登录而且查询的是本人,返回token - data = { - info: data, - token: ctx.session._snsToken - } - } ctx.success(data) } else { ctx.fail() @@ -130,7 +123,7 @@ exports.delete = async (ctx, next) => { exports.me = async (ctx, next) => { const data = await UserModel - .findOne({ name: config.author }) + .findOne({ name: config.author, role: 0 }) .select('-password -role -createdAt -updatedAt -github') .exec() .catch(err => { diff --git a/server/plugins/redis.js b/server/plugins/redis.js index c8f312c..b3d0314 100644 --- a/server/plugins/redis.js +++ b/server/plugins/redis.js @@ -30,7 +30,8 @@ exports.connect = () => { client.on('reconnecting', () => debug('正在重连中...')) } -exports.set = (key = '', value = '') => new Promise((resolve, reject) => { +// 默认 1小时 过期 +exports.set = (key = '', value = '', expired = 60 * 60) => new Promise((resolve, reject) => { if (connected) { if (!isType(value, 'String')) { try { @@ -40,7 +41,7 @@ exports.set = (key = '', value = '') => new Promise((resolve, reject) => { value = value.toString() } } - client.set(key, value, (err, res) => { + client.set(key, value, 'EX', expired, (err, res) => { if (err) { debug.error('存储【 %s 】失败,错误:%s', key, err.message) return reject(err) diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 16bc418..0d30735 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -18,10 +18,6 @@ const { auth, moment } = require('../controller') -// const { authenticate } = require('../middleware') -// const isSnsAuthenticated = authenticate.isSnsAuthenticated() -// const snsAuth = authenticate.snsAuth -// const snsLogout = authenticate.snsLogout() // Article router.get('/articles', article.list) @@ -53,13 +49,8 @@ router.get('/options', option.data) // User router.get('/users/me', user.me) -// router.get('/users/:id', isSnsAuthenticated, user.item) // Auth -// router.get('/auth/info', isSnsAuthenticated, auth.info) -// router.get('/auth/logout', isSnsAuthenticated, auth.logout) -// router.get('/auth/github/login', snsAuth('github')) -// .get('/callback', auth.githubLogin) router.get('/auth/github/token', auth.fetchGithubToken) router.get('/auth/github/user', auth.fetchGithubUser) diff --git a/server/service/crontab.js b/server/service/crontab.js index 6838d36..18727da 100644 --- a/server/service/crontab.js +++ b/server/service/crontab.js @@ -12,10 +12,6 @@ exports.start = () => { option.updateOptionLinks() setInterval(option.updateOptionLinks.bind(option), 1000 * 60 * 60 * 1) - // 音乐 每10分钟更新一次 - music.updateMusicCache() - setInterval(music.updateMusicCache.bind(music), 1000 * 60 * 10) - // 用户 每1天更新一次 user.updateGithubInfo() setInterval(user.updateGithubInfo.bind(user), 1000 * 60 * 60 * 24) From 64dd6893041b06d390b95f151c355f481fb7e250 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 8 Nov 2017 22:15:55 +0800 Subject: [PATCH 070/208] [update] update music and config --- server/config/index.js | 1 + server/controller/music.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/config/index.js b/server/config/index.js index 95ef1ba..055d087 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -30,6 +30,7 @@ const baseConfig = { articleLimit: 15, commentLimit: 99, momentLimit: 10, + // 垃圾评论允许的最大发布次数 commentSpamLimit: 3, mongo: { option: { diff --git a/server/controller/music.js b/server/controller/music.js index a40f13b..34c7c04 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -17,7 +17,7 @@ const expired = 60 * 10 // 过期时间10分钟 const isProd = process.env.NODE_ENV === 'production' const debug = getDebug('Music') const neteaseMusic = new NeteseMusic() -const cacheKey = 'musicData' +const cacheKey = 'music-data' exports.list = async (ctx, next) => { // 后台实时获取 From 03502c33c835add0a8ad6ebdb51104b57cefb636 Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 13 Nov 2017 17:35:30 +0800 Subject: [PATCH 071/208] [update] upgrade simple-netease-cloud-music module --- package.json | 2 +- server/controller/music.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e92dc01..8905d4e 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "nodemailer": "^4.3.1", "passport-github": "^1.1.0", "redis": "^2.8.0", - "simple-netease-cloud-music": "^0.1.8" + "simple-netease-cloud-music": "^0.2.0" }, "devDependencies": { "cross-env": "^5.0.5", diff --git a/server/controller/music.js b/server/controller/music.js index 34c7c04..160b904 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -115,8 +115,8 @@ exports.cover = async (ctx, next) => { // 获取除了歌曲链接和歌词外其他信息 function fetchSonglist (playListId) { - // return neteaseMusic.playlist(playListId).then(({ playlist }) => { - return fetchNE('playlist', playListId).then(({ playlist }) => { + return neteaseMusic._playlist(playListId).then(({ playlist }) => { + // return fetchNE('playlist', playListId).then(({ playlist }) => { return !playlist ? [] : playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { return { id, From 616ed97280984c1d3603704572803a3058dba04b Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 28 Dec 2017 00:41:13 +0800 Subject: [PATCH 072/208] [feature] add hot articles api --- server/config/index.js | 1 + server/controller/article.js | 29 +++++++++++++++++++++++++++++ server/routes/frontend.js | 1 + 3 files changed, 31 insertions(+) diff --git a/server/config/index.js b/server/config/index.js index 055d087..80f53d4 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -28,6 +28,7 @@ const baseConfig = { '10001': 'params error' }, articleLimit: 15, + hotLimit: 9, commentLimit: 99, momentLimit: 10, // 垃圾评论允许的最大发布次数 diff --git a/server/controller/article.js b/server/controller/article.js index e8dac71..8b52357 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -148,6 +148,35 @@ exports.list = async (ctx, next) => { } } +exports.hot = async (ctx, next) => { + const limit = ctx.validateQuery('limit').defaultTo(config.hotLimit).toInt().gt(0, 'the "limit" parameter should be greater than 0').val() + const data = await ArticleModel.find() + .sort('-meta.comments -meta.ups -meta.pvs') + .select('-content -renderedContent -state') + .populate([ + { + path: 'category', + select: 'name' + }, + { + path: 'tag', + select: 'name' + } + ]) + .limit(limit) + .catch(err => { + ctx.log.error(err.message) + return null + }) + if (data) { + ctx.success({ + list: data + }) + } else { + ctx.fail() + } +} + exports.item = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 0d30735..aacb5bf 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -21,6 +21,7 @@ const { // Article router.get('/articles', article.list) +router.get('/articles/hot', article.hot) router.get('/articles/:id', article.item) router.post('/articles/:id/like', article.like) From b52a01172f98e491d5fc0c78be0f2d85df7a0fbd Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 3 Jan 2018 01:49:03 +0800 Subject: [PATCH 073/208] [fix] fix simple-netease-cloud-music song url fetch bug, and change the email to iamjooger@gmail.com --- package.json | 2 +- server/app.js | 2 +- server/config/development.js | 2 +- server/config/index.js | 6 +++--- server/config/production.js | 2 +- server/config/test.js | 2 +- server/controller/article.js | 11 ++++++----- server/controller/auth.js | 2 +- server/controller/category.js | 2 +- server/controller/comment.js | 2 +- server/controller/index.js | 2 +- server/controller/moment.js | 2 +- server/controller/music.js | 6 ++++-- server/controller/option.js | 2 +- server/controller/statistics.js | 2 +- server/controller/tag.js | 2 +- server/controller/user.js | 2 +- server/middleware/authenticate.js | 2 +- server/middleware/error.js | 2 +- server/middleware/formidable.js | 2 +- server/middleware/header.js | 2 +- server/middleware/index.js | 2 +- server/middleware/response.js | 2 +- server/model/index.js | 2 +- server/model/schema/article.js | 2 +- server/model/schema/category.js | 8 ++++++-- server/model/schema/comment.js | 2 +- server/model/schema/index.js | 2 +- server/model/schema/log.js | 2 +- server/model/schema/moment.js | 2 +- server/model/schema/option.js | 2 +- server/model/schema/tag.js | 8 ++++++-- server/model/schema/user.js | 2 +- server/plugins/akismet.js | 2 +- server/plugins/gc.js | 2 +- server/plugins/index.js | 2 +- server/plugins/mailer.js | 2 +- server/plugins/mongo.js | 2 +- server/plugins/redis.js | 2 +- server/plugins/validation.js | 2 +- server/routes/backend.js | 2 +- server/routes/frontend.js | 2 +- server/routes/index.js | 2 +- server/service/crontab.js | 2 +- server/service/github-passport.js | 2 +- server/service/github-token.js | 2 +- server/service/github-userinfo.js | 2 +- server/service/index.js | 2 +- server/service/netease-music.js | 2 +- server/util/debug.js | 2 +- server/util/encrypt.js | 2 +- server/util/index.js | 2 +- server/util/location.js | 2 +- server/util/marked.js | 2 +- server/util/proxy.js | 2 +- server/util/sign-token.js | 2 +- 56 files changed, 76 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index 8905d4e..49b5643 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "author": "Jooger", "site": "https://jooger.me", - "email": "zzy1198258955@163.com", + "email": "iamjooger@gmail.com", "repository": { "type": "https", "url": "https://github.com/jo0ger/jooger.me-server.git" diff --git a/server/app.js b/server/app.js index 0817df8..4c70f65 100644 --- a/server/app.js +++ b/server/app.js @@ -1,6 +1,6 @@ /** * @desc Server entry - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/config/development.js b/server/config/development.js index 914343b..dc659a2 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -1,6 +1,6 @@ /** * @desc 开发环境配置 - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/config/index.js b/server/config/index.js index 80f53d4..51d03cb 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -1,6 +1,6 @@ /** * @desc Config entry - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ @@ -27,8 +27,8 @@ const baseConfig = { '500': 'server error', '10001': 'params error' }, - articleLimit: 15, - hotLimit: 9, + articleLimit: 3, + hotLimit: 5, commentLimit: 99, momentLimit: 10, // 垃圾评论允许的最大发布次数 diff --git a/server/config/production.js b/server/config/production.js index 8ce7ba8..adebd19 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -1,6 +1,6 @@ /** * @desc 开发环境配置 - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/config/test.js b/server/config/test.js index cf2a5df..48404c8 100644 --- a/server/config/test.js +++ b/server/config/test.js @@ -1,6 +1,6 @@ /** * @desc 测试环境配置 - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/controller/article.js b/server/controller/article.js index 8b52357..276fa5d 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -1,6 +1,6 @@ /** * @desc Article controller - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ @@ -42,7 +42,7 @@ exports.list = async (ctx, next) => { populate: [ { path: 'category', - select: 'name description' + select: 'name description extends' }, { path: 'tag', @@ -192,11 +192,11 @@ exports.item = async (ctx, next) => { data = await queryPs.populate([ { path: 'category', - select: 'name description' + select: 'name description extends' }, { path: 'tag', - select: 'name description' + select: 'name description extends' } ]).exec().catch(err => { ctx.log.error(err.message) @@ -348,10 +348,11 @@ exports.delete = async (ctx, next) => { exports.like = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const like = ctx.validateBody('like').optional().defaultTo(true).toBoolean().val() const data = await ArticleModel.findByIdAndUpdate(id, { $inc: { - 'meta.ups': 1 + 'meta.ups': like ? 1 : -1 } }).catch(err => { ctx.log.error(err.message) diff --git a/server/controller/auth.js b/server/controller/auth.js index 25b1344..7635430 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -1,6 +1,6 @@ /** * @desc Auth controller - * @author Jooger + * @author Jooger * @date 27 Sep 2017 */ diff --git a/server/controller/category.js b/server/controller/category.js index 57b6ed0..3dee7d6 100644 --- a/server/controller/category.js +++ b/server/controller/category.js @@ -1,6 +1,6 @@ /** * @desc Category controll - * @author Jooger + * @author Jooger * @date 26 Oct 2017 */ diff --git a/server/controller/comment.js b/server/controller/comment.js index 428d968..78ef74c 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -1,6 +1,6 @@ /** * @desc Comment controller - * @author Jooger + * @author Jooger * @date 28 Oct 2017 */ diff --git a/server/controller/index.js b/server/controller/index.js index 99deb1d..9f7c9a2 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -1,6 +1,6 @@ /** * @desc Controllers entry - * @author Jooger + * @author Jooger * @date 26 Sep 2017 */ diff --git a/server/controller/moment.js b/server/controller/moment.js index cf9d2d7..8b42138 100644 --- a/server/controller/moment.js +++ b/server/controller/moment.js @@ -1,6 +1,6 @@ /** * @desc Moment controller - * @author Jooger + * @author Jooger * @date 30 Oct 2017 */ diff --git a/server/controller/music.js b/server/controller/music.js index 160b904..d18469a 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -1,6 +1,6 @@ /** * @desc Music controller - * @author Jooger + * @author Jooger * @date 26 Sep 2017 */ @@ -73,7 +73,9 @@ exports.url = async (ctx, next) => { .isString('the "song_id" parameter should be String type') .val() - const data = await neteaseMusic.url(songId).then(data => { + // BUG: 库出错了,暂时不用此库请求URL + // const data = await neteaseMusic.url(songId).then(data => { + const data = await fetchNE('songUrl', songId).then(data => { if (!isProd) { return data.data || [] } diff --git a/server/controller/option.js b/server/controller/option.js index bad824d..f39d5f6 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -1,6 +1,6 @@ /** * @desc Option controller - * @author Jooger + * @author Jooger * @date 26 Sep 2017 */ diff --git a/server/controller/statistics.js b/server/controller/statistics.js index 79862bb..94567a4 100644 --- a/server/controller/statistics.js +++ b/server/controller/statistics.js @@ -1,6 +1,6 @@ /** * @desc Statistics controller - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/controller/tag.js b/server/controller/tag.js index 4c1bd9a..5de2c7e 100644 --- a/server/controller/tag.js +++ b/server/controller/tag.js @@ -1,6 +1,6 @@ /** * @desc Tag controller - * @author Jooger + * @author Jooger * @date 26 Sep 2017 */ diff --git a/server/controller/user.js b/server/controller/user.js index ede7cd1..8f9a06b 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -1,6 +1,6 @@ /** * @desc User controlelr - * @author Jooger + * @author Jooger * @date 26 Sep 2017 */ diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index 091c011..54e31bd 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -1,6 +1,6 @@ /** * @desc Auth middleware - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/middleware/error.js b/server/middleware/error.js index 265b31c..e067b40 100644 --- a/server/middleware/error.js +++ b/server/middleware/error.js @@ -1,6 +1,6 @@ /** * @desc Error monitor - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/middleware/formidable.js b/server/middleware/formidable.js index 96a784b..ecc4d94 100644 --- a/server/middleware/formidable.js +++ b/server/middleware/formidable.js @@ -1,6 +1,6 @@ /** * @desc Formidable 上传中间件,暂未使用 - * @author Jooger + * @author Jooger * @date 10 Oct 2017 */ diff --git a/server/middleware/header.js b/server/middleware/header.js index 3ef3e56..9af9ed1 100644 --- a/server/middleware/header.js +++ b/server/middleware/header.js @@ -1,6 +1,6 @@ /** * @desc 设置相应头 - * @author Jooger + * @author Jooger * @date 26 Sep 2017 */ diff --git a/server/middleware/index.js b/server/middleware/index.js index d1ef6b3..589f382 100644 --- a/server/middleware/index.js +++ b/server/middleware/index.js @@ -1,6 +1,6 @@ /** * @desc Middleware Entry - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/middleware/response.js b/server/middleware/response.js index 6f4c7e1..5768475 100644 --- a/server/middleware/response.js +++ b/server/middleware/response.js @@ -1,6 +1,6 @@ /** * @desc Reponse middleware - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/model/index.js b/server/model/index.js index 224a844..3bef373 100644 --- a/server/model/index.js +++ b/server/model/index.js @@ -1,6 +1,6 @@ /** * @desc Models entry - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/model/schema/article.js b/server/model/schema/article.js index f89ef60..027169d 100644 --- a/server/model/schema/article.js +++ b/server/model/schema/article.js @@ -1,6 +1,6 @@ /** * @desc - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/model/schema/category.js b/server/model/schema/category.js index 401d5dc..cb61977 100644 --- a/server/model/schema/category.js +++ b/server/model/schema/category.js @@ -1,6 +1,6 @@ /** * @desc Category - * @author Jooger + * @author Jooger * @date 26 Oct 2017 */ @@ -12,7 +12,11 @@ const categorySchema = new mongoose.Schema({ name: { type: String, required: true }, description: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now } + updatedAt: { type: Date, default: Date.now }, + extends: [{ + key: { type: String, validate: /\S+/ }, + value: { type: String, validate: /\S+/ } + }] }) module.exports = categorySchema diff --git a/server/model/schema/comment.js b/server/model/schema/comment.js index d09a61e..cb26280 100644 --- a/server/model/schema/comment.js +++ b/server/model/schema/comment.js @@ -1,6 +1,6 @@ /** * @desc - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/model/schema/index.js b/server/model/schema/index.js index 6c747a4..f81642a 100644 --- a/server/model/schema/index.js +++ b/server/model/schema/index.js @@ -1,6 +1,6 @@ /** * @desc Schemas entry - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/model/schema/log.js b/server/model/schema/log.js index 8d58c9c..d56c16b 100644 --- a/server/model/schema/log.js +++ b/server/model/schema/log.js @@ -1,6 +1,6 @@ /** * @desc Site Log - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/model/schema/moment.js b/server/model/schema/moment.js index 51cb1ac..edc5cbb 100644 --- a/server/model/schema/moment.js +++ b/server/model/schema/moment.js @@ -1,6 +1,6 @@ /** * @desc 个人动态 Model - * @author Jooger + * @author Jooger * @date 30 Oct 2017 */ diff --git a/server/model/schema/option.js b/server/model/schema/option.js index ac03885..57b972d 100644 --- a/server/model/schema/option.js +++ b/server/model/schema/option.js @@ -1,6 +1,6 @@ /** * @desc Option schema - * @author Jooger + * @author Jooger * @date 26 Sep 2017 */ diff --git a/server/model/schema/tag.js b/server/model/schema/tag.js index d742988..2258493 100644 --- a/server/model/schema/tag.js +++ b/server/model/schema/tag.js @@ -1,6 +1,6 @@ /** * @desc Tag - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ @@ -12,7 +12,11 @@ const tagSchema = new mongoose.Schema({ name: { type: String, required: true }, description: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now } + updatedAt: { type: Date, default: Date.now }, + extends: [{ + key: { type: String, validate: /\S+/ }, + value: { type: String, validate: /\S+/ } + }] }) module.exports = tagSchema diff --git a/server/model/schema/user.js b/server/model/schema/user.js index bb8bff2..431c63f 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -1,6 +1,6 @@ /** * @desc Admin schema - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/plugins/akismet.js b/server/plugins/akismet.js index e5aad8f..e2b3bde 100644 --- a/server/plugins/akismet.js +++ b/server/plugins/akismet.js @@ -1,6 +1,6 @@ /** * @desc Akismet - * @author Jooger + * @author Jooger * @date 29 Oct 2017 */ diff --git a/server/plugins/gc.js b/server/plugins/gc.js index 7e8f431..b8b23b3 100644 --- a/server/plugins/gc.js +++ b/server/plugins/gc.js @@ -1,6 +1,6 @@ /** * @desc V8 GC - * @author Jooger + * @author Jooger * @date 30 Oct 2017 */ diff --git a/server/plugins/index.js b/server/plugins/index.js index 57dec53..cea5b60 100644 --- a/server/plugins/index.js +++ b/server/plugins/index.js @@ -1,6 +1,6 @@ /** * @desc Plugins entry - * @author Jooger + * @author Jooger * @date 29 Oct 2017 */ diff --git a/server/plugins/mailer.js b/server/plugins/mailer.js index d7cf425..fee9598 100644 --- a/server/plugins/mailer.js +++ b/server/plugins/mailer.js @@ -1,6 +1,6 @@ /** * @desc Mail plugin - * @author Jooger + * @author Jooger * @date 29 Oct 2017 */ diff --git a/server/plugins/mongo.js b/server/plugins/mongo.js index b1280a2..c3b3cad 100644 --- a/server/plugins/mongo.js +++ b/server/plugins/mongo.js @@ -1,6 +1,6 @@ /** * @desc Mongodb connect - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/plugins/redis.js b/server/plugins/redis.js index b3d0314..b0f46e2 100644 --- a/server/plugins/redis.js +++ b/server/plugins/redis.js @@ -1,6 +1,6 @@ /** * @desc Redis connect - * @author Jooger + * @author Jooger * @date 27 Oct 2017 */ diff --git a/server/plugins/validation.js b/server/plugins/validation.js index ac050fc..ea592c2 100644 --- a/server/plugins/validation.js +++ b/server/plugins/validation.js @@ -1,6 +1,6 @@ /** * @desc Custom Validations for koa-bouncer - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/routes/backend.js b/server/routes/backend.js index b562c0f..5342344 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -1,6 +1,6 @@ /** * @desc backend api map - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/routes/frontend.js b/server/routes/frontend.js index aacb5bf..59bd278 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -1,6 +1,6 @@ /** * @desc front api map - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/routes/index.js b/server/routes/index.js index 791d770..9315331 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,6 +1,6 @@ /** * @desc Routes entry - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/service/crontab.js b/server/service/crontab.js index 18727da..de2e90f 100644 --- a/server/service/crontab.js +++ b/server/service/crontab.js @@ -1,6 +1,6 @@ /** * @desc 定时任务 - * @author Jooger + * @author Jooger * @date 27 Oct 2017 */ diff --git a/server/service/github-passport.js b/server/service/github-passport.js index 30a1425..6731be7 100644 --- a/server/service/github-passport.js +++ b/server/service/github-passport.js @@ -1,6 +1,6 @@ /** * @desc github password service - * @author Jooger + * @author Jooger * @date 27 Sep 2017 */ diff --git a/server/service/github-token.js b/server/service/github-token.js index a11ffbe..a765231 100644 --- a/server/service/github-token.js +++ b/server/service/github-token.js @@ -1,6 +1,6 @@ /** * @desc Github access_token - * @author Jooger + * @author Jooger * @date 2 Nov 2017 */ diff --git a/server/service/github-userinfo.js b/server/service/github-userinfo.js index 5999491..3ca2f00 100644 --- a/server/service/github-userinfo.js +++ b/server/service/github-userinfo.js @@ -1,6 +1,6 @@ /** * @desc github userinfo fetch service - * @author Jooger + * @author Jooger * @date 27 Sep 2017 */ diff --git a/server/service/index.js b/server/service/index.js index cbeafb4..c26a0de 100644 --- a/server/service/index.js +++ b/server/service/index.js @@ -1,6 +1,6 @@ /** * @desc Services entry - * @author Jooger + * @author Jooger * @date 27 Sep 2017 */ diff --git a/server/service/netease-music.js b/server/service/netease-music.js index 1374a10..c1c2352 100644 --- a/server/service/netease-music.js +++ b/server/service/netease-music.js @@ -1,6 +1,6 @@ /** * @desc 网易云音乐 TEST (暂未使用) - * @author Jooger + * @author Jooger * @date 30 Sep 2017 */ diff --git a/server/util/debug.js b/server/util/debug.js index 983d266..f430bb8 100644 --- a/server/util/debug.js +++ b/server/util/debug.js @@ -1,6 +1,6 @@ /** * @desc Debug wrapper for debug - * @author Jooger + * @author Jooger * @date 27 Sep 2017 */ diff --git a/server/util/encrypt.js b/server/util/encrypt.js index 922f7c4..5660a4f 100644 --- a/server/util/encrypt.js +++ b/server/util/encrypt.js @@ -1,6 +1,6 @@ /** * @desc - * @author Jooger + * @author Jooger * @date 30 Sep 2017 */ diff --git a/server/util/index.js b/server/util/index.js index 00da0e0..2d3d3d2 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -1,6 +1,6 @@ /** * @desc Util entry - * @author Jooger + * @author Jooger * @date 25 Sep 2017 */ diff --git a/server/util/location.js b/server/util/location.js index 929c44e..7e199c6 100644 --- a/server/util/location.js +++ b/server/util/location.js @@ -1,6 +1,6 @@ /** * @desc 获取ip和location - * @author Jooger + * @author Jooger * @date 30 Oct 2017 */ diff --git a/server/util/marked.js b/server/util/marked.js index 66d7f6b..506f2bb 100644 --- a/server/util/marked.js +++ b/server/util/marked.js @@ -1,6 +1,6 @@ /** * @desc Markdown renderer - * @author Jooger + * @author Jooger * @date 26 Sep 2017 */ diff --git a/server/util/proxy.js b/server/util/proxy.js index df85c15..9baf49c 100644 --- a/server/util/proxy.js +++ b/server/util/proxy.js @@ -1,6 +1,6 @@ /** * @desc Http url replace to "/proxy/..." - * @author Jooger + * @author Jooger * @date 20 Oct 2017 */ diff --git a/server/util/sign-token.js b/server/util/sign-token.js index 5dc1208..e58dade 100644 --- a/server/util/sign-token.js +++ b/server/util/sign-token.js @@ -1,6 +1,6 @@ /** * @desc jwt sign token - * @author Jooger + * @author Jooger * @date 27 Sep 2017 */ From 5fcf53beabb7c76fa177aaf62b630975d0aaf4a6 Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 4 Jan 2018 02:27:43 +0800 Subject: [PATCH 074/208] [update] update music controller --- server/config/index.js | 2 +- server/controller/music.js | 18 +++++++++++++----- server/service/crontab.js | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index 51d03cb..47c664c 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -28,7 +28,7 @@ const baseConfig = { '10001': 'params error' }, articleLimit: 3, - hotLimit: 5, + hotLimit: 7, commentLimit: 99, momentLimit: 10, // 垃圾评论允许的最大发布次数 diff --git a/server/controller/music.js b/server/controller/music.js index d18469a..c7d0b63 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -45,12 +45,12 @@ exports.list = async (ctx, next) => { // hit if (musicData && musicData.id === playListId) { - return ctx.success(musicData.list || []) + return ctx.success(musicData.data || []) } // update cache const data = await exports.updateMusicCache(playListId) - ctx.success(data && data.list || []) + ctx.success(data && data.data || {}) } } @@ -118,8 +118,10 @@ exports.cover = async (ctx, next) => { // 获取除了歌曲链接和歌词外其他信息 function fetchSonglist (playListId) { return neteaseMusic._playlist(playListId).then(({ playlist }) => { - // return fetchNE('playlist', playListId).then(({ playlist }) => { - return !playlist ? [] : playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { + if (!playlist) { + return null + } + const tracks = playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { return { id, name, @@ -133,6 +135,12 @@ function fetchSonglist (playListId) { tns: tns || [] } }) + return { + tracks, + name: playlist.name, + description: playlist.description, + tags: playlist.tags + } }).catch(err => { debug.error('歌单列表获取失败,错误:', err.message) return null @@ -168,7 +176,7 @@ exports.updateMusicCache = async function (playListId = '') { } const set = { id: playListId, - list: data + data } // 设置10分钟过期 diff --git a/server/service/crontab.js b/server/service/crontab.js index de2e90f..39bcbf8 100644 --- a/server/service/crontab.js +++ b/server/service/crontab.js @@ -7,7 +7,7 @@ 'use strict' exports.start = () => { - const { option, music, user } = require('../controller') + const { option, user } = require('../controller') // 友链 每1小时更新一次 option.updateOptionLinks() setInterval(option.updateOptionLinks.bind(option), 1000 * 60 * 60 * 1) From 83de6a890f860a1d79effe68da5827d13e91d738 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Thu, 4 Jan 2018 16:59:45 +0800 Subject: [PATCH 075/208] [update] category list add rank params, for sorting --- server/controller/article.js | 4 ++-- server/controller/category.js | 21 +++++++++++++++++++-- server/model/schema/category.js | 2 ++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/server/controller/article.js b/server/controller/article.js index 276fa5d..cec1d61 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -224,7 +224,7 @@ exports.create = async (ctx, next) => { .notEmpty() .isString('the "content" parameter should be String type') .val() - const keywords = ctx.validateBody('keywords').optional().defaultTo([]).isArray('the "keywords" parameter should be Array type').val() + const keywords = ctx.validateBody('keywords').defaultTo([]).isArray('the "keywords" parameter should be Array type').val() const category = ctx.validateBody('category').optional().isObjectId().val() const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() const description = ctx.validateBody('description') @@ -348,7 +348,7 @@ exports.delete = async (ctx, next) => { exports.like = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - const like = ctx.validateBody('like').optional().defaultTo(true).toBoolean().val() + const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() const data = await ArticleModel.findByIdAndUpdate(id, { $inc: { diff --git a/server/controller/category.js b/server/controller/category.js index 3dee7d6..ec2ecd1 100644 --- a/server/controller/category.js +++ b/server/controller/category.js @@ -10,6 +10,7 @@ const { CategoryModel, ArticleModel } = require('../model') exports.list = async (ctx, next) => { const keyword = ctx.validateQuery('keyword').optional().toString().val() + const rank = ctx.validateQuery('rank').defaultTo(1).toInt().isIn([0, 1], 'the "rank" parameter is not the expected value').val() const query = {} // 搜索关键词 @@ -20,7 +21,12 @@ exports.list = async (ctx, next) => { ] } - const data = await CategoryModel.find(query).sort('-createdAt').catch(err => { + let sort = '-createdAt' + if (rank) { + sort = 'list ' + sort + } + + const data = await CategoryModel.find(query).sort(sort).catch(err => { ctx.log.error(err.message) return null }) @@ -77,6 +83,15 @@ exports.create = async (ctx, next) => { .optional() .isString('the "description" parameter should be String type') .val() + const list = ctx.validateBody('list') + .defaultTo(1) + .toInt() + .isNumeric('the "list" parameter should be Number type') + .val() + const ext = ctx.validateBody('extends') + .optional() + .isArray('the "extends" parameter should be Array type') + .val() const { length } = await CategoryModel.find({ name }).exec().catch(err => { ctx.log.error(err.message) @@ -86,7 +101,9 @@ exports.create = async (ctx, next) => { if (!length) { const data = await new CategoryModel({ name, - description + description, + extends: ext, + list }).save().catch(err => { ctx.log.error(err.message) return null diff --git a/server/model/schema/category.js b/server/model/schema/category.js index cb61977..774883f 100644 --- a/server/model/schema/category.js +++ b/server/model/schema/category.js @@ -13,6 +13,8 @@ const categorySchema = new mongoose.Schema({ description: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, + // 排序 首页分类展示顺序 + list: { type: Number, default: 1 }, extends: [{ key: { type: String, validate: /\S+/ }, value: { type: String, validate: /\S+/ } From 9c752b1d680a2014378a0b33e08cde0becb77d93 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Thu, 4 Jan 2018 19:05:46 +0800 Subject: [PATCH 076/208] [feature] add article archives api --- README.md | 2 ++ server/controller/article.js | 52 +++++++++++++++++++++++++++++++++++- server/routes/frontend.js | 1 + server/util/index.js | 16 +++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba95025..5903b96 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ jooger.me-server * ~~个人动态api~~ (2017.10.30) +* ~~文章归档api~~(2018.01.04) + * 邮件模板 * 消息api diff --git a/server/controller/article.js b/server/controller/article.js index cec1d61..5533f10 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -8,7 +8,7 @@ const config = require('../config') const { ArticleModel, CategoryModel, TagModel } = require('../model') -const { marked, isObjectId, createObjectId, getDebug } = require('../util') +const { marked, isObjectId, createObjectId, getDebug, getMonthFromNum } = require('../util') const debug = getDebug('Article') exports.list = async (ctx, next) => { @@ -366,6 +366,56 @@ exports.like = async (ctx, next) => { } } +exports.archive = async (ctx, next) => { + let data = await ArticleModel.aggregate([ + { $match: { state: 1 } }, + { + $project: { + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + title: 1, + createdAt: 1 + } + }, + { + $group: { + _id: { + year: '$year', + month: '$month' + }, + articles: { + $push: { + title: '$title', + _id: '$_id', + create_at: '$createdAt' + } + } + } + } + ]) + + if (data && data.length) { + data = [...new Set(data.map(item => item._id.year))].map(year => { + const months = [] + data.forEach(item => { + const { _id, articles } = item + if (year === _id.year) { + months.push({ + month: _id.month, + monthStr: getMonthFromNum(_id.month), + articles + }) + } + }) + return { + year, + months + } + }) + } + ctx.success(data || []) +} + /** * 根据标签获取相关文章 * @param {} ctx koa ctx diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 59bd278..d8f446b 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -22,6 +22,7 @@ const { // Article router.get('/articles', article.list) router.get('/articles/hot', article.hot) +router.get('/articles/archives', article.archive) router.get('/articles/:id', article.item) router.post('/articles/:id/like', article.like) diff --git a/server/util/index.js b/server/util/index.js index 2d3d3d2..e91a10d 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -59,3 +59,19 @@ exports.randomString = (length = 8) => { } return id } + +const monthMap = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' +] +exports.getMonthFromNum = (num = 1) => monthMap[num - 1] || '' From 8110aef2eb1053b0b1302017019287672c966714 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Thu, 4 Jan 2018 19:16:35 +0800 Subject: [PATCH 077/208] [fix] fix article archives sorting error, desc to asc --- server/controller/article.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/controller/article.js b/server/controller/article.js index 5533f10..db05c62 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -369,6 +369,7 @@ exports.like = async (ctx, next) => { exports.archive = async (ctx, next) => { let data = await ArticleModel.aggregate([ { $match: { state: 1 } }, + { $sort: { createdAt: 1 } }, { $project: { year: { $year: '$createdAt' }, From 6831a2bc32badd4005c5c3db4cfc2a5457227c4f Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Thu, 4 Jan 2018 20:08:11 +0800 Subject: [PATCH 078/208] [fix] fix archive api bug --- server/controller/article.js | 9 +++++++-- server/controller/tag.js | 7 ++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/server/controller/article.js b/server/controller/article.js index db05c62..0439948 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -388,19 +388,21 @@ exports.archive = async (ctx, next) => { $push: { title: '$title', _id: '$_id', - create_at: '$createdAt' + createdAt: '$createdAt' } } } } ]) + let count = 0 if (data && data.length) { data = [...new Set(data.map(item => item._id.year))].map(year => { const months = [] data.forEach(item => { const { _id, articles } = item if (year === _id.year) { + count += articles.length months.push({ month: _id.month, monthStr: getMonthFromNum(_id.month), @@ -414,7 +416,10 @@ exports.archive = async (ctx, next) => { } }) } - ctx.success(data || []) + ctx.success({ + count, + list: data || [] + }) } /** diff --git a/server/controller/tag.js b/server/controller/tag.js index 5de2c7e..6ae5f0d 100644 --- a/server/controller/tag.js +++ b/server/controller/tag.js @@ -77,6 +77,10 @@ exports.create = async (ctx, next) => { .optional() .isString('the "description" parameter should be String type') .val() + const ext = ctx.validateBody('extends') + .optional() + .isArray('the "extends" parameter should be Array type') + .val() const { length } = await TagModel.find({ name }).exec().catch(err => { ctx.log.error(err.message) @@ -86,7 +90,8 @@ exports.create = async (ctx, next) => { if (!length) { const data = await new TagModel({ name, - description + description, + extends: ext }).save().catch(err => { ctx.log.error(err.message) return null From ca42462902a4895ce087cad7ec08e21559d2eecf Mon Sep 17 00:00:00 2001 From: Jooger Date: Fri, 5 Jan 2018 02:25:45 +0800 Subject: [PATCH 079/208] [update] update user apis --- package.json | 25 ++++++----- server/config/development.js | 2 - server/config/index.js | 16 ++++--- server/config/production.js | 2 - server/controller/comment.js | 2 +- server/controller/user.js | 70 +++++++++++++++++-------------- server/model/schema/user.js | 17 ++++---- server/plugins/mongo.js | 18 ++++---- server/service/github-userinfo.js | 2 +- 9 files changed, 81 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index 49b5643..283543b 100644 --- a/package.json +++ b/package.json @@ -3,18 +3,12 @@ "version": "1.3.0", "private": true, "description": "🔥 My blog's api server build by koa2 and mongoose", - "scripts": { - "dev": "cross-env NODE_ENV=development nodemon bin/www", - "debug": "cross-env NODE_ENV=development nodemon --inspect bin/www", - "prod": "cross-env NODE_ENV=production nodemon bin/www", - "pm2": "pm2 startOrReload ecosystem.config.js", - "pm2:prod": "pm2 startOrReload ecosystem.config.js --env production", - "deploy": "pm2 deploy ecosystem.config.js production", - "test": "echo \"Error: no test specified\" && exit 1" + "homepage": "https://github.com/jo0ger/jooger.me-server", + "author": { + "name": "jo0ger", + "email": "iamjooger@gmail.com", + "url": "https://jooger.me" }, - "author": "Jooger", - "site": "https://jooger.me", - "email": "iamjooger@gmail.com", "repository": { "type": "https", "url": "https://github.com/jo0ger/jooger.me-server.git" @@ -32,6 +26,15 @@ "url": "https://github.com/jo0ger/jooger.me-server/issues" }, "bin": "./node_modules/.bin/", + "scripts": { + "dev": "cross-env NODE_ENV=development nodemon bin/www", + "debug": "cross-env NODE_ENV=development nodemon --inspect bin/www", + "prod": "cross-env NODE_ENV=production nodemon bin/www", + "pm2": "pm2 startOrReload ecosystem.config.js", + "pm2:prod": "pm2 startOrReload ecosystem.config.js --env production", + "deploy": "pm2 deploy ecosystem.config.js production", + "test": "echo \"Error: no test specified\" && exit 1" + }, "dependencies": { "akismet-api": "^3.0.0", "axios": "^0.16.2", diff --git a/server/config/development.js b/server/config/development.js index dc659a2..dbff1c6 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -6,8 +6,6 @@ 'use strict' -const packageInfo = require('../../package.json') - module.exports = { mongo: { uri: 'mongodb://127.0.0.1/jooger-me-dev' diff --git a/server/config/index.js b/server/config/index.js index 47c664c..3e7a960 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -13,9 +13,9 @@ const packageInfo = require('../../package.json') const baseConfig = { name: packageInfo.name, version: packageInfo.version, - author: packageInfo.author || 'Jooger', - site: packageInfo.site, - email: packageInfo.email, + author: packageInfo.author.name, + site: packageInfo.author.url, + email: packageInfo.author.email, env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3001, @@ -27,9 +27,15 @@ const baseConfig = { '500': 'server error', '10001': 'params error' }, + // 角色 + roleMap: { + ADMIN: 0, + USER: 1, + GITHUB_USER: 2 + }, articleLimit: 3, hotLimit: 7, - commentLimit: 99, + commentLimit: 20, momentLimit: 10, // 垃圾评论允许的最大发布次数 commentSpamLimit: 3, @@ -56,7 +62,7 @@ const baseConfig = { userCookieKey: 'jooger.me.userid', secrets: `${packageInfo.name}-secrets`, // 初始化管理员,默认github账户名 - defaultName: 'jo0ger', + defaultName: packageInfo.author.name, defaultPassword: 'admin_jooger', // 允许请求的域名 allowedOrigins: [ diff --git a/server/config/production.js b/server/config/production.js index adebd19..73ac5e3 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -6,8 +6,6 @@ 'use strict' -const packageInfo = require('../../package.json') - module.exports = { mongo: { uri: 'mongodb://127.0.0.1/jooger-me' diff --git a/server/controller/comment.js b/server/controller/comment.js index 78ef74c..3f8a125 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -40,7 +40,7 @@ exports.list = async (ctx, next) => { populate: [ { path: 'author', - select: !ctx._isAuthenticated ? 'github avatar name' : '' + select: !ctx._isAuthenticated ? 'github avatar name site' : '' }, { path: 'parent', diff --git a/server/controller/user.js b/server/controller/user.js index 8f9a06b..88ad685 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -14,6 +14,7 @@ const debug = getDebug('User') exports.list = async (ctx, next) => { let select = '-password' + if (!ctx._isAuthenticated) { select += ' -createdAt -updatedAt -role' } @@ -35,13 +36,13 @@ exports.list = async (ctx, next) => { exports.item = async (ctx, next) => { const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - let select = '-password -role -createdAt -updatedAt' - - if (ctx._isAuthenticated) { - select = '-password' + let select = '-password' + + if (!ctx._isAuthenticated) { + select += ' -createdAt -updatedAt -role -github' } - let data = await UserModel.findById(id) + const data = await UserModel.findById(id) .select(select) .exec() .catch(err => { @@ -58,18 +59,20 @@ exports.item = async (ctx, next) => { exports.update = async (ctx, next) => { const name = ctx.validateBody('name').optional().isString('the "name" parameter should be String type').val() - const password = ctx.validateBody('password').optional().isString('the "password" parameter should be String type').val() - const slogan = ctx.validateBody('slogan').optional().isString('the "slogan" parameter should be String type').val() - const description = ctx.validateBody('description').optional().isString('the "description" parameter should be String type').val() + const email = ctx.validateBody('email').optional().isString('the "email" parameter should be String type').isEmail('Invalid email format').val() + const site = ctx.validateBody('site').optional().isString('the "site" parameter should be String type').val() const avatar = ctx.validateBody('avatar').optional().isString('the "avatar" parameter should be String type').val() - const role = ctx.validateBody('role').optional().toInt().isIn([0, 1], 'the "role" parameter is not the expected value').val() + const slogan = ctx.validateBody('slogan').optional().isString('the "slogan" parameter should be String type').val() + const role = ctx.validateBody('role').optional().toInt().isIn(Object.values(config.roleMap), 'the "role" parameter is not the expected value').val() + const password = ctx.validateBody('password').optional().isString('the "password" parameter should be String type').val() const mute = ctx.validateBody('mute').optional().toBoolean().val() const user = {} name && (user.name = name) slogan && (user.slogan = slogan) - description && (user.description = description) + site && (user.site = site) avatar && (user.avatar = avatar) + email && (user.email = email) if (role !== undefined) { user.role = role @@ -123,7 +126,7 @@ exports.delete = async (ctx, next) => { exports.me = async (ctx, next) => { const data = await UserModel - .findOne({ name: config.author, role: 0 }) + .findOne({ 'github.login': config.author, role: 0 }) .select('-password -role -createdAt -updatedAt -github') .exec() .catch(err => { @@ -146,33 +149,36 @@ exports.updateGithubInfo = async () => { debug.error('用户查找失败,错误:', err.message) return [] }) - const updates = await getGithubUsersInfo(users.map(user => user.github.login)) + const githubUsers = users.reduce((sum, user) => { + if (user.role === config.roleMap.GITHUB_USER || (user.role === config.roleMap.ADMIN && user.github.login)) { + sum.push(user) + } + return sum + }, []) + const updates = await getGithubUsersInfo(githubUsers.map(user => user.github.login)) Promise.all( - updates.map((data, index) => { - if (!data) { - return null - } - const user = users[index] + updates.reduce((tasks, data, index) => { + const user = githubUsers[index] const u = { + name: data.name, + email: data.email, + avatar: proxy(data.avatar_url), + site: data.blog || data.url, + slogan: data.bio, github: { id: data.id, - email: data.email, - login: data.login, - name: data.name, - blog: data.blog || data.url + login: data.login } } - // 非管理员更新其他信息,管理员只更新github信息 - if (user.role !== 0) { - u.name = data.name - u.slogan = data.bio - u.avatar = proxy(data.avatar_url) - } - return UserModel.findByIdAndUpdate(user._id, u).exec().catch(err => { - debug.error('用户Github信息更新失败,错误:', err.message) - }) - }).filter(ps => !!ps) + tasks.push( + UserModel.findByIdAndUpdate(user._id, u).exec().catch(err => { + debug.error('Github用户信息更新失败,错误:', err.message) + return null + }) + ) + return tasks + }, []) ).then(() => { - debug.success('全部用户Github信息更新成功') + debug.success('所有Github用户信息更新成功') }) } diff --git a/server/model/schema/user.js b/server/model/schema/user.js index 431c63f..a98e49a 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -11,24 +11,21 @@ const config = require('../../config') const userSchema = new mongoose.Schema({ name: { type: String, default: config.auth.defaultName, required: true }, - password: { - type: String, - default: '' - }, - slogan: { type: String, default: '' }, - avatar: { type: String, default: '' }, - // 角色 0 管理员 | 1 普通用户 + email: { type: String, required: true, validate: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/ }, + avatar: { type: String, required: true }, + site: { type: String, validate: /^((https|http):\/\/)+[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/ }, + slogan: { type: String }, + // 角色 0 管理员 | 1 普通用户 | 2 github用户 role: { type: Number, default: 1 }, + // role = 0的时候才有该项 + password: { type: String }, // 是否被禁言 mute: { type: Boolean, default: false }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, github: { id: { type: String, default: '' }, - email: { type: String, default: '' }, login: { type: String, default: '' }, - name: { type: String, default: '' }, - blog: { type: String, default: '' }, } }) diff --git a/server/plugins/mongo.js b/server/plugins/mongo.js index c3b3cad..36fa389 100644 --- a/server/plugins/mongo.js +++ b/server/plugins/mongo.js @@ -47,7 +47,10 @@ async function seedOption () { // 管理员初始化 async function seedAdmin () { - const admin = await UserModel.findOne({ role: 0 }).exec() + const admin = await UserModel.findOne({ + role: config.roleMap.ADMIN, + 'github.login': config.author + }).exec() .catch(err => debug.error('初始化管理员查询失败,错误:', err.message)) if (!admin) { createAdmin() @@ -62,23 +65,20 @@ async function createAdmin () { } data = data[0] const result = await new UserModel({ - role: 0, + role: config.roleMap.ADMIN, name: data.name, + email: data.email, password: bhash(config.auth.defaultPassword), slogan: data.bio, + site: data.blog || data.url, avatar: proxy(data.avatar_url), github: { id: data.id, - email: data.email, - login: data.login, - name: data.name, - blog: data.blog || data.url + login: data.login } }) .save() - .catch(err => { - fail(err.message) - }) + .catch(err => fail(err.message)) if (!result || !result._id) { fail('本地入库失败') diff --git a/server/service/github-userinfo.js b/server/service/github-userinfo.js index 3ca2f00..d17aac2 100644 --- a/server/service/github-userinfo.js +++ b/server/service/github-userinfo.js @@ -33,7 +33,7 @@ exports.getGithubUsersInfo = (githubNames = '') => { } }).then(res => { if (res && res.status === 200) { - debug.success('抓取【 %s 】信息成功', name,) + debug.success('抓取【 %s 】信息成功', name) return res.data } return null From e4820c06253cab086a84e62727c9ac83da67991c Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 7 Jan 2018 01:29:06 +0800 Subject: [PATCH 080/208] [update] add gravatar support --- package.json | 1 + server/config/index.js | 1 + server/controller/article.js | 2 +- server/controller/category.js | 6 ++- server/controller/comment.js | 90 ++++++++++++++++++++++++++++------- server/controller/user.js | 2 +- server/routes/frontend.js | 1 + server/util/gravatar.js | 30 ++++++++++++ server/util/index.js | 2 + 9 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 server/util/gravatar.js diff --git a/package.json b/package.json index 283543b..026a13d 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "formidable": "^1.1.1", "gc-stats": "^1.0.2", "geoip-lite": "^1.2.1", + "gravatar": "^1.6.0", "highlight.js": "^9.12.0", "idle-gc": "^1.0.1", "jsonwebtoken": "^8.1.0", diff --git a/server/config/index.js b/server/config/index.js index 3e7a960..93e65a1 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -61,6 +61,7 @@ const baseConfig = { }, userCookieKey: 'jooger.me.userid', secrets: `${packageInfo.name}-secrets`, + defaultAvatar: 'http://static.jooger.me/img/common/default-avatar.png', // 初始化管理员,默认github账户名 defaultName: packageInfo.author.name, defaultPassword: 'admin_jooger', diff --git a/server/controller/article.js b/server/controller/article.js index 0439948..6d38610 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -268,7 +268,7 @@ exports.create = async (ctx, next) => { if (!data.permalink) { // 更新永久链接 data = await ArticleModel.findByIdAndUpdate(data._id, { - permalink: `${config.site}/blog/article/${data._id}` + permalink: `${config.site}/article/${data._id}` }, { new : true }).exec().catch(err => { diff --git a/server/controller/category.js b/server/controller/category.js index ec2ecd1..bace315 100644 --- a/server/controller/category.js +++ b/server/controller/category.js @@ -86,7 +86,6 @@ exports.create = async (ctx, next) => { const list = ctx.validateBody('list') .defaultTo(1) .toInt() - .isNumeric('the "list" parameter should be Number type') .val() const ext = ctx.validateBody('extends') .optional() @@ -129,11 +128,16 @@ exports.update = async (ctx, next) => { .optional() .isString('the "description" parameter should be String type') .val() + const list = ctx.validateBody('list') + .optional() + .toInt() + .val() const tag = {} name && (tag.name = name) description && (tag.description = description) + list && (tag.list = list) const data = await CategoryModel.findByIdAndUpdate(id, tag, { new: true diff --git a/server/controller/comment.js b/server/controller/comment.js index 3f8a125..71cedd0 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -9,7 +9,7 @@ const config = require('../config') const { akismet, mailer } = require('../plugins') const { CommentModel, UserModel, ArticleModel } = require('../model') -const { marked, isObjectId, createObjectId, getDebug, getLocation } = require('../util') +const { isType, marked, isObjectId, createObjectId, getDebug, getLocation, gravatar } = require('../util') const debug = getDebug('Comment') const isProd = process.env.NODE_ENV === 'development' @@ -190,7 +190,7 @@ exports.list = async (ctx, next) => { exports.item = async (ctx, next) => { const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() - + let data = null let queryPs = null if (!ctx._isAuthenticated) { @@ -231,13 +231,14 @@ exports.create = async (ctx, next) => { .notEmpty() .isString('内容参数必须是字符串类型') .val() - const author = ctx.validateBody('author').required('用户ID参数必填').toString().isObjectId('用户ID参数无效').val() const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], '置顶参数无效').val() const type = ctx.validateBody('type').defaultTo(0).toInt().isIn([0, 1], '评论类型参数无效').val() const article = ctx.validateBody('article').optional().toString().isObjectId('文章ID参数无效').val() const parent = ctx.validateBody('parent').optional().toString().isObjectId('父评论ID参数无效').val() const forward = ctx.validateBody('forward').optional().toString().isObjectId('前置评论ID参数无效').val() + // ObjectId | { id, name, email, site } + const author = ctx.validateBody('author').required('作者参数无效').val() const req = ctx.req const comment = { content } @@ -247,23 +248,19 @@ exports.create = async (ctx, next) => { } comment.article = article } - + if (parent && !forward || !parent && forward) { return ctx.fail('父评论ID和前置评论ID必须同时存在') } - - const user = await UserModel.findById(author).select('github').exec().catch(err => { - debug.error('用户查找失败,错误:', err.message) - ctx.log.error(err.message) - return null - }) + const user = await checkAuthor.call(ctx, author) if (!user) { - return ctx.fail('用户不存在') + return ctx.fail('作者不存在') } else if (user.mute) { // 如果被禁言 return ctx.fail('您已经被禁言') } + comment.author = user._id if (!checkUserSpam(user)) { return ctx.fail('您的垃圾评论数量已达到最大限制,已被禁言') @@ -300,9 +297,9 @@ exports.create = async (ctx, next) => { referrer : comment.meta.referer, // Required! permalink, comment_type : getCommentType(type), - comment_author : user.github.login, - comment_author_email : user.github.email, - comment_author_url : user.github.blog, + comment_author : user.name, + comment_author_email : user.email, + comment_author_url : user.site, comment_content : content, is_test : isProd }) @@ -316,7 +313,6 @@ exports.create = async (ctx, next) => { parent && (comment.parent = parent) forward && (comment.forward = forward) comment.renderedContent = marked(content) - comment.author = author let data = await new CommentModel(comment).save().catch(err => { ctx.log.error(err.message) @@ -329,7 +325,7 @@ exports.create = async (ctx, next) => { p = p.select('-content -state -updatedAt') .populate({ path: 'author', - select: 'name github' + select: 'name site avatar role mute email' }) .populate({ path: 'parent', @@ -462,10 +458,11 @@ exports.delete = async (ctx, next) => { exports.like = async (ctx, next) => { const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() const data = await CommentModel.findByIdAndUpdate(id, { $inc: { - ups: 1 + ups: like ? 1 : -1 } }).catch(err => { ctx.log.error(err.message) @@ -605,3 +602,62 @@ async function sendEmailToAdminAndUser (comment, permalink) { } } } + +// 验证作者 +async function checkAuthor (author) { + let user = null + if (isObjectId(author)) { + user = await findUser({ + _id: author + }) + } else if (isType(author, 'Object')) { + // 需要创建或更新用户 + const update = {} + author.name && (update.name = author.name) + author.site && (update.site = author.site) + if (author.email) { + update.avatar = gravatar(author.email) + update.email = author.email + } + if (author.id) { + // 更新 + if (isObjectId(author.id)) { + user = await UserModel.findByIdAndUpdate(author.id, update, { + new : true + }).exec().catch(err => { + debug.error('用户更新失败,错误:', err.message) + this.log.error(err.message) + return null + }) + if (user) { + debug.success(`用户【${user.name}】更新成功`) + } + } + } else { + // 创建 + user = await new UserModel({ + ...update, + role: config.roleMap.USER + }) + .save() + .catch(err => { + debug.error('用户创建失败,错误:', err.message) + this.log.error(err.message) + return null + }) + if (user) { + debug.success(`用户【${user.name}】创建成功`) + } + } + } + return user +} + +async function findUser (query = {}, update) { + const user = await UserModel.findOne(query).select('-password').exec().catch(err => { + debug.error('用户查找失败,错误:', err.message) + ctx.log.error(err.message) + return null + }) + return user +} diff --git a/server/controller/user.js b/server/controller/user.js index 88ad685..6ebeeaf 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -39,7 +39,7 @@ exports.item = async (ctx, next) => { let select = '-password' if (!ctx._isAuthenticated) { - select += ' -createdAt -updatedAt -role -github' + select += ' -createdAt -updatedAt -github' } const data = await UserModel.findById(id) diff --git a/server/routes/frontend.js b/server/routes/frontend.js index d8f446b..212caf6 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -51,6 +51,7 @@ router.get('/options', option.data) // User router.get('/users/me', user.me) +router.get('/users/:id', user.item) // Auth router.get('/auth/github/token', auth.fetchGithubToken) diff --git a/server/util/gravatar.js b/server/util/gravatar.js new file mode 100644 index 0000000..c3e99c3 --- /dev/null +++ b/server/util/gravatar.js @@ -0,0 +1,30 @@ +/** + * @desc gravatar头像 + * @author Jooger + * @date 6 Jan 2018 + */ + +'use strict' + +const gravatar = require('gravatar') +const config = require('../config') +const { isEmail } = require('./') + +const isProd = process.env.NODE_ENV === 'production' + +module.exports = (email = '', opt = {}) => { + if (!/^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(email)) { + return config.auth.defaultAvatar + } + + const protocol = `http${isProd ? 's' : ''}` + const url = gravatar.url(email, { + s: '100', + r: 'x', + d: 'retro', + protocol, + ...opt + }) + + return url.replace(`${protocol}://`, 'https://jooger.me/proxy/') +} diff --git a/server/util/index.js b/server/util/index.js index e91a10d..53f504c 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -21,6 +21,8 @@ exports.proxy = require('./proxy') exports.getLocation = require('./location') +exports.gravatar = require('./gravatar') + exports.noop = function () {} exports.isType = (obj = {}, type = 'Object') => { From 83e5d0973c4d5524b87749d3bc88dddf7a09a787 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 7 Jan 2018 02:55:18 +0800 Subject: [PATCH 081/208] [update] update response code map --- server/config/index.js | 12 ++++++------ server/controller/comment.js | 4 ++-- server/controller/music.js | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index 93e65a1..4691fb9 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -20,12 +20,12 @@ const baseConfig = { root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3001, codeMap: { - '-1': 'fail', - '200': 'success', - '401': 'authentication failure', - '403': 'forbidden', - '500': 'server error', - '10001': 'params error' + '-1': '请求失败', + '200': '请求成功', + '401': '权限校验失败', + '403': 'Forbidden', + '500': '服务器错误', + '10001': '参数错误' }, // 角色 roleMap: { diff --git a/server/controller/comment.js b/server/controller/comment.js index 71cedd0..cb9e6e5 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -342,7 +342,7 @@ exports.create = async (ctx, next) => { ctx.log.error(err.message) return null }) - ctx.success(data) + ctx.success(data, '评论成功') // 如果是文章评论,则更新文章评论数量 if (type === 0) { updateArticleCommentCount([comment.article]) @@ -350,7 +350,7 @@ exports.create = async (ctx, next) => { // 发送邮件通知站主和被评论者 sendEmailToAdminAndUser(data, permalink) } else { - ctx.fail() + ctx.fail('评论失败') } } diff --git a/server/controller/music.js b/server/controller/music.js index c7d0b63..63761c1 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -37,7 +37,7 @@ exports.list = async (ctx, next) => { }) if (!option || !option.musicId) { - return ctx.fail() + return ctx.fail('歌单未找到') } const playListId = option.musicId From 3cbc970e1aeeb8bb3b7b39df1ccf071ccfff8322 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 9 Jan 2018 01:07:09 +0800 Subject: [PATCH 082/208] [update] add validator --- package.json | 3 ++- server/model/schema/user.js | 5 +++-- server/plugins/mailer.js | 8 ++++++-- server/util/index.js | 12 ++++++++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 026a13d..7130f28 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "nodemailer": "^4.3.1", "passport-github": "^1.1.0", "redis": "^2.8.0", - "simple-netease-cloud-music": "^0.2.0" + "simple-netease-cloud-music": "^0.2.0", + "validator": "^9.2.0" }, "devDependencies": { "cross-env": "^5.0.5", diff --git a/server/model/schema/user.js b/server/model/schema/user.js index a98e49a..1a57474 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -8,12 +8,13 @@ const mongoose = require('mongoose') const config = require('../../config') +const { isEmail, isSiteUrl } = require('../../util') const userSchema = new mongoose.Schema({ name: { type: String, default: config.auth.defaultName, required: true }, - email: { type: String, required: true, validate: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/ }, + email: { type: String, required: true, validate: isEmail }, avatar: { type: String, required: true }, - site: { type: String, validate: /^((https|http):\/\/)+[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/ }, + site: { type: String, validate: isSiteUrl }, slogan: { type: String }, // 角色 0 管理员 | 1 普通用户 | 2 github用户 role: { type: Number, default: 1 }, diff --git a/server/plugins/mailer.js b/server/plugins/mailer.js index fee9598..3c72ee3 100644 --- a/server/plugins/mailer.js +++ b/server/plugins/mailer.js @@ -10,19 +10,23 @@ const nodemailer = require('nodemailer') const config = require('../config') const { getDebug } = require('../util') const debug = getDebug('Mailer') +const isProd = process.env.NODE_ENV === 'production' let isVerify = false -const transporter = nodemailer.createTransport({ +const transporter = isProd ? nodemailer.createTransport({ service: '163', secure: true, auth: { user: config.email, pass: process.env['163Pass'] || '163邮箱密码' } -}) +}) : null exports.start = async () => { return new Promise((resolve, reject) => { + if (!transporter) { + return + } transporter.verify((err, success) => { if (err) { isVerify = false diff --git a/server/util/index.js b/server/util/index.js index 53f504c..5c96dbc 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -8,6 +8,7 @@ const bcrypt = require('bcryptjs') const mongoose = require('mongoose') +const validator = require('validator') exports.getDebug = require('./debug') @@ -77,3 +78,14 @@ const monthMap = [ 'December' ] exports.getMonthFromNum = (num = 1) => monthMap[num - 1] || '' + +Object.keys(validator).forEach(key => { + exports[key] = function () { + return validator[key].apply(validator, arguments) + } +}) + +exports.isSiteUrl = (site = '') => validator.isURL(site, { + protocols: ['http', 'https'], + require_protocol: true +}) From 3d4f5571dbff25f6686bd77680a625c8f792062c Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Tue, 9 Jan 2018 19:05:32 +0800 Subject: [PATCH 083/208] [update] update commen type --- server/controller/comment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/controller/comment.js b/server/controller/comment.js index cb9e6e5..548c75d 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -494,10 +494,10 @@ function getPermalink (comment = {}) { function getCommentType (type) { switch (type) { case 0: - return '博客文章评论' + return '文章评论' break case 1: - return '个人站点留言' + return '站点留言' default: return '评论' break From 0821a9c2cd7533ad3341de77922769c7ef79715e Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 11 Jan 2018 01:36:40 +0800 Subject: [PATCH 084/208] [update] update user model, and user update controller --- server/controller/user.js | 6 +++++- server/model/schema/user.js | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/server/controller/user.js b/server/controller/user.js index 6ebeeaf..9d82f92 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -61,6 +61,7 @@ exports.update = async (ctx, next) => { const name = ctx.validateBody('name').optional().isString('the "name" parameter should be String type').val() const email = ctx.validateBody('email').optional().isString('the "email" parameter should be String type').isEmail('Invalid email format').val() const site = ctx.validateBody('site').optional().isString('the "site" parameter should be String type').val() + const description = ctx.validateBody('description').optional().isString('the "description" parameter should be String type').val() const avatar = ctx.validateBody('avatar').optional().isString('the "avatar" parameter should be String type').val() const slogan = ctx.validateBody('slogan').optional().isString('the "slogan" parameter should be String type').val() const role = ctx.validateBody('role').optional().toInt().isIn(Object.values(config.roleMap), 'the "role" parameter is not the expected value').val() @@ -71,6 +72,7 @@ exports.update = async (ctx, next) => { name && (user.name = name) slogan && (user.slogan = slogan) site && (user.site = site) + description && (user.description = description) avatar && (user.avatar = avatar) email && (user.email = email) @@ -127,7 +129,7 @@ exports.delete = async (ctx, next) => { exports.me = async (ctx, next) => { const data = await UserModel .findOne({ 'github.login': config.author, role: 0 }) - .select('-password -role -createdAt -updatedAt -github') + .select('-password -role -createdAt -updatedAt -github -mute') .exec() .catch(err => { ctx.log.error(err.message) @@ -141,6 +143,8 @@ exports.me = async (ctx, next) => { } } +exports.guest = async (ctx, next) => {} + // 更新用户的Github信息 exports.updateGithubInfo = async () => { const users = await UserModel.find({}) diff --git a/server/model/schema/user.js b/server/model/schema/user.js index 1a57474..49fdab2 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -16,6 +16,7 @@ const userSchema = new mongoose.Schema({ avatar: { type: String, required: true }, site: { type: String, validate: isSiteUrl }, slogan: { type: String }, + description: { type: String, default: '' }, // 角色 0 管理员 | 1 普通用户 | 2 github用户 role: { type: Number, default: 1 }, // role = 0的时候才有该项 From 8a2a5072e6e9e7eb860deff4c0bc560811c10cb7 Mon Sep 17 00:00:00 2001 From: Jooger Date: Fri, 12 Jan 2018 01:39:04 +0800 Subject: [PATCH 085/208] [update] upgrade dep --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7130f28..0ee6383 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "highlight.js": "^9.12.0", "idle-gc": "^1.0.1", "jsonwebtoken": "^8.1.0", - "koa": "^2.2.0", + "koa": "^2.4.1", "koa-bodyparser": "^3.2.0", "koa-bouncer": "^6.0.0", "koa-bunyan-logger": "^2.0.0", @@ -62,13 +62,13 @@ "koa-router": "^7.1.1", "koa-session": "^5.5.0", "lodash": "^4.17.4", - "marked": "^0.3.6", - "mongoose": "^4.12.4", + "marked": "^0.3.12", + "mongoose": "^4.13.9", "mongoose-paginate": "^5.0.3", - "nodemailer": "^4.3.1", + "nodemailer": "^4.4.1", "passport-github": "^1.1.0", "redis": "^2.8.0", - "simple-netease-cloud-music": "^0.2.0", + "simple-netease-cloud-music": "^0.3.0", "validator": "^9.2.0" }, "devDependencies": { From b3297796b823dca605fc660c220b85067bbd6748 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Fri, 12 Jan 2018 19:19:20 +0800 Subject: [PATCH 086/208] [feature] add user guests api --- server/controller/user.js | 29 +++++++++++++++++++++++++++-- server/routes/frontend.js | 1 + 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/server/controller/user.js b/server/controller/user.js index 9d82f92..a6fb046 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -7,7 +7,7 @@ 'use strict' const config = require('../config') -const { UserModel } = require('../model') +const { UserModel, CommentModel } = require('../model') const { bhash, bcompare, getDebug, proxy } = require('../util') const { getGithubUsersInfo } = require('../service') const debug = getDebug('User') @@ -143,7 +143,32 @@ exports.me = async (ctx, next) => { } } -exports.guest = async (ctx, next) => {} +exports.guests = async (ctx, next) => { + // $lookup尝试失败,只能循环查询用户了 + let data = await CommentModel.aggregate([ + { + $match: { + $nor: [ + { + 'github.login': config.author, + role: 0 + } + ] + } + }, + { $sort: { createdAt: -1 } }, + { + $group: { + _id: '$author' + } + } + ]) + const list = await Promise.all((data || []).map(item => UserModel.findById(item._id).select('name avatar site'))) + ctx.success({ + list, + total: list.length + }) +} // 更新用户的Github信息 exports.updateGithubInfo = async () => { diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 212caf6..b1fd256 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -51,6 +51,7 @@ router.get('/options', option.data) // User router.get('/users/me', user.me) +router.get('/users/guests', user.guests) router.get('/users/:id', user.item) // Auth From 9d740d43f90b4a12e0ab110a3e3d6bcc1c54d117 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 14 Jan 2018 16:04:02 +0800 Subject: [PATCH 087/208] [fix] fix music bug --- server/controller/music.js | 1 + server/controller/user.js | 26 ++++++++++++++++---------- server/routes/frontend.js | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index 63761c1..770934d 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -136,6 +136,7 @@ function fetchSonglist (playListId) { } }) return { + id: playListId, tracks, name: playlist.name, description: playlist.description, diff --git a/server/controller/user.js b/server/controller/user.js index a6fb046..c80ed7b 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -126,7 +126,7 @@ exports.delete = async (ctx, next) => { } } -exports.me = async (ctx, next) => { +exports.blogger = async (ctx, next) => { const data = await UserModel .findOne({ 'github.login': config.author, role: 0 }) .select('-password -role -createdAt -updatedAt -github -mute') @@ -147,23 +147,29 @@ exports.guests = async (ctx, next) => { // $lookup尝试失败,只能循环查询用户了 let data = await CommentModel.aggregate([ { - $match: { - $nor: [ - { - 'github.login': config.author, - role: 0 - } - ] + $sort: { + createdAt: -1 } }, - { $sort: { createdAt: -1 } }, { $group: { _id: '$author' } } ]) - const list = await Promise.all((data || []).map(item => UserModel.findById(item._id).select('name avatar site'))) + let list = await Promise.all((data || []).map((item) => { + return UserModel.findOne({ + _id: item._id, + $nor: [ + { + role: config.roleMap.ADMIN + }, { + 'github.login': config.author + } + ] + }).select('name site avatar') + })) + list = list.filter(item => !!item) ctx.success({ list, total: list.length diff --git a/server/routes/frontend.js b/server/routes/frontend.js index b1fd256..435c8ab 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -50,7 +50,7 @@ router.get('/music/songs/:song_id/lyric', music.lyric) router.get('/options', option.data) // User -router.get('/users/me', user.me) +router.get('/users/blogger', user.blogger) router.get('/users/guests', user.guests) router.get('/users/:id', user.item) From 0297162432976c65089f99a7f294193b02157e4d Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 16 Jan 2018 23:23:16 +0800 Subject: [PATCH 088/208] [update] user model add location and company properties --- server/controller/user.js | 3 +++ server/model/schema/user.js | 4 +++- server/plugins/mongo.js | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/server/controller/user.js b/server/controller/user.js index c80ed7b..0365d80 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -193,6 +193,7 @@ exports.updateGithubInfo = async () => { const updates = await getGithubUsersInfo(githubUsers.map(user => user.github.login)) Promise.all( updates.reduce((tasks, data, index) => { + console.log(data) const user = githubUsers[index] const u = { name: data.name, @@ -200,6 +201,8 @@ exports.updateGithubInfo = async () => { avatar: proxy(data.avatar_url), site: data.blog || data.url, slogan: data.bio, + company: data.company, + location: data.location, github: { id: data.id, login: data.login diff --git a/server/model/schema/user.js b/server/model/schema/user.js index 49fdab2..6a14bf2 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -23,11 +23,13 @@ const userSchema = new mongoose.Schema({ password: { type: String }, // 是否被禁言 mute: { type: Boolean, default: false }, + company: { type: String, default: '' }, + location: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, github: { id: { type: String, default: '' }, - login: { type: String, default: '' }, + login: { type: String, default: '' } } }) diff --git a/server/plugins/mongo.js b/server/plugins/mongo.js index 36fa389..cf6b515 100644 --- a/server/plugins/mongo.js +++ b/server/plugins/mongo.js @@ -72,6 +72,8 @@ async function createAdmin () { slogan: data.bio, site: data.blog || data.url, avatar: proxy(data.avatar_url), + company: data.company, + location: data.location, github: { id: data.id, login: data.login From d22282d99ec1999eab1234b1d2594099d77c69a3 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 27 Jan 2018 02:32:54 +0800 Subject: [PATCH 089/208] [update] add user model proxy --- server/app.js | 3 + server/config/index.js | 60 ++++---- server/controller/article.js | 245 ++++++++++++-------------------- server/controller/comment.js | 6 +- server/controller/moment.js | 2 +- server/controller/user.js | 7 +- server/middleware/error.js | 8 +- server/middleware/header.js | 2 +- server/middleware/response.js | 6 +- server/plugins/mongo.js | 4 +- server/plugins/validation.js | 8 +- server/proxy/article.js | 71 +++++++++ server/proxy/index.js | 11 ++ server/routes/index.js | 2 +- server/service/crontab.js | 4 +- server/service/netease-music.js | 2 +- server/util/debug.js | 2 +- server/util/encrypt.js | 2 +- server/util/index.js | 30 ++-- 19 files changed, 256 insertions(+), 219 deletions(-) create mode 100644 server/proxy/article.js create mode 100644 server/proxy/index.js diff --git a/server/app.js b/server/app.js index 4c70f65..4b02e61 100644 --- a/server/app.js +++ b/server/app.js @@ -50,6 +50,9 @@ app.use(koaBunyanLogger({ name: packageInfo.name, level: 'debug' })) +app.use(koaBunyanLogger.requestIdContext({ + header: 'Request-Id' +})) app.use(bouncer.middleware()) app.use(middlewares.response) app.use(middlewares.error) diff --git a/server/config/index.js b/server/config/index.js index 4691fb9..e50db79 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -19,26 +19,17 @@ const baseConfig = { env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3001, - codeMap: { - '-1': '请求失败', - '200': '请求成功', - '401': '权限校验失败', - '403': 'Forbidden', - '500': '服务器错误', - '10001': '参数错误' + // 限制参数 + limit: { + articleLimit: 3, + // 相关文章限制个数 + relatedArticleLimit: 10, + hotLimit: 7, + commentLimit: 20, + momentLimit: 10, + // 垃圾评论允许的最大发布次数 + commentSpamLimit: 3, }, - // 角色 - roleMap: { - ADMIN: 0, - USER: 1, - GITHUB_USER: 2 - }, - articleLimit: 3, - hotLimit: 7, - commentLimit: 20, - momentLimit: 10, - // 垃圾评论允许的最大发布次数 - commentSpamLimit: 3, mongo: { option: { useMongoClient: true, @@ -64,13 +55,7 @@ const baseConfig = { defaultAvatar: 'http://static.jooger.me/img/common/default-avatar.png', // 初始化管理员,默认github账户名 defaultName: packageInfo.author.name, - defaultPassword: 'admin_jooger', - // 允许请求的域名 - allowedOrigins: [ - 'jooger.me', - 'www.jooger.me', - 'admin.jooger.me' - ] + defaultPassword: 'admin_jooger' }, sns: { github: { @@ -83,6 +68,29 @@ const baseConfig = { }, akismet: { apiKey: process.env.akismetApikey || 'akismet api key' + }, + constant: { + // 允许请求的域名 + allowedOrigins: [ + 'jooger.me', + 'www.jooger.me', + 'admin.jooger.me' + ], + codeMap: { + '-1': '请求失败', + '200': '请求成功', + '401': '权限校验失败', + '403': 'Forbidden', + '500': '服务器错误', + '10001': '参数错误' + }, + // 角色 + roleMap: { + ADMIN: 0, + USER: 1, + GITHUB_USER: 2 + }, + monthMap: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] } } diff --git a/server/controller/article.js b/server/controller/article.js index 6d38610..95bf80b 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -7,14 +7,16 @@ 'use strict' const config = require('../config') +const { ArticleProxy } = require('../proxy') const { ArticleModel, CategoryModel, TagModel } = require('../model') -const { marked, isObjectId, createObjectId, getDebug, getMonthFromNum } = require('../util') +const { marked, isObjectId, createObjectId, getDebug, getMonthFromNum, getDocsPagination } = require('../util') const debug = getDebug('Article') +// 文章列表 exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.articleLimit).toInt().gt(0, 'the "per_page" parameter should be greater than 0').val() - const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'the "page" parameter should be greater than 0').val() - const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() + const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.articleLimit).toInt().gt(0, 'per_page参数必须大于0').val() + const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'page参数必须大于0').val() + const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'state参数错误').val() const category = ctx.validateQuery('category').optional().toString().val() const tag = ctx.validateQuery('tag').optional().toString().val() const keyword = ctx.validateQuery('keyword').optional().toString().val() @@ -25,17 +27,19 @@ exports.list = async (ctx, next) => { // -1 desc | 1 asc const order = ctx.validateQuery('order').optional().toInt().isIn( [-1, 1], - 'invalid "order" parameter, optional value: -1 or 1' + 'order参数错误' ).val() // createdAt | updatedAt | publishedAt | meta.ups | meta.pvs | meta.comments const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn( ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], - 'invalid "sort_by" parameter' + 'sort_by参数错误' ).val() // 过滤条件 const options = { - sort: { createdAt: -1 }, + sort: { + createdAt: -1 + }, page, limit: pageSize, select: '-content -renderedContent', @@ -43,8 +47,7 @@ exports.list = async (ctx, next) => { { path: 'category', select: 'name description extends' - }, - { + }, { path: 'tag', select: 'name description' } @@ -128,117 +131,81 @@ exports.list = async (ctx, next) => { } } - const articles = await ArticleModel.paginate(query, options).catch(err => { - ctx.log.error(err.message) - return null - }) + const articles = await ArticleProxy.paginate(query, options) - if (articles) { - ctx.success({ - list: articles.docs, - pagination: { - total: articles.total, - current_page: articles.page > articles.pages ? articles.pages : articles.page, - total_page: articles.pages, - per_page: articles.limit - } - }) - } else { - ctx.fail(-1, 'the article list access failed') - } + articles + ? ctx.success(getDocsPaginationData(articles), '文章列表获取成功') + : ctx.fail('文章列表获取失败') } +// 热门文章 exports.hot = async (ctx, next) => { - const limit = ctx.validateQuery('limit').defaultTo(config.hotLimit).toInt().gt(0, 'the "limit" parameter should be greater than 0').val() - const data = await ArticleModel.find() + const limit = ctx.validateQuery('limit').defaultTo(config.limit.hotLimit).toInt().gt(0, 'limit参数必须大于0').val() + const data = await ArticleProxy.find() .sort('-meta.comments -meta.ups -meta.pvs') .select('-content -renderedContent -state') .populate([ { path: 'category', select: 'name' - }, - { + }, { path: 'tag', select: 'name' } ]) .limit(limit) - .catch(err => { - ctx.log.error(err.message) - return null - }) - if (data) { - ctx.success({ - list: data - }) - } else { - ctx.fail() - } + data + ? ctx.success({ list: data }, '热门文章获取成功') + : ctx.fail('热门文章获取失败') } +// 文章详情 exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() let data = null - let queryPs = null + let query = null // 只有前台博客访问文章的时候pv才+1 if (!ctx._isAuthenticated) { - queryPs = ArticleModel.findOneAndUpdate({ _id: id, state: 1 }, { $inc: { 'meta.pvs': 1 } }, { new: true }).select('-content') + query = ArticleProxy.updateOne({ _id: id, state: 1 }, { $inc: { 'meta.pvs': 1 } }).select('-content') } else { - queryPs = ArticleModel.findById(id) + query = ArticleProxy.getById(id) } - data = await queryPs.populate([ + data = await query.populate([ { path: 'category', select: 'name description extends' - }, - { + }, { path: 'tag', select: 'name description extends' } - ]).exec().catch(err => { - ctx.log.error(err.message) - return null - }) + ]).exec() if (data) { data = data.toObject() - await getRelatedArticles(ctx, data) - await getSiblingArticles(ctx, data) - ctx.success(data) + await Promise.all([ + getRelatedArticles(ctx, data), + getSiblingArticles(ctx, data) + ]) + ctx.success(data, '文章详情获取成功') } else { - ctx.fail(-1, 'the article not found') + ctx.fail('文章详情获取失败') } } +// 文章创建 exports.create = async (ctx, next) => { - const title = ctx.validateBody('title') - .required('the "title" parameter is required') - .notEmpty() - .isString('the "title" parameter should be String type') - .val() - const content = ctx.validateBody('content') - .required('the "content" parameter is required') - .notEmpty() - .isString('the "content" parameter should be String type') - .val() - const keywords = ctx.validateBody('keywords').defaultTo([]).isArray('the "keywords" parameter should be Array type').val() + const title = ctx.validateBody('title').required('缺少文章标题').notEmpty().val() + const content = ctx.validateBody('content').required('缺少文章内容').notEmpty().val() + const keywords = ctx.validateBody('keywords').optional().toArray().val() const category = ctx.validateBody('category').optional().isObjectId().val() const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() - const description = ctx.validateBody('description') - .optional() - .isString('the "description" parameter should be String type') - .val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() - const thumb = ctx.validateBody('thumb').optional().isString('the "thumb" parameter should be String type').val() + const description = ctx.validateBody('description').optional().val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数错误').val() + const thumb = ctx.validateBody('thumb').optional().val() const createdAt = ctx.validateBody('createdAt').optional().toString().val() - const permalink = ctx.validateBody('permalink') - .optional() - .isString('the "permalink" parameter should be String type') - .val() - + const permalink = ctx.validateBody('permalink').optional().val() const article = {} title && (article.title = title) @@ -253,52 +220,41 @@ exports.create = async (ctx, next) => { if (state !== undefined) { article.state = state } + article.content = content + article.renderedContent = marked(content) - if (content) { - article.content = content - article.renderedContent = marked(content) - } - - let data = await new ArticleModel(article).save().catch(err => { - ctx.log.error(err.message) - return null - }) + let data = await ArticleProxy.newAndSave(article) - if (data) { + if (data && data.length) { + data = data[0] if (!data.permalink) { // 更新永久链接 - data = await ArticleModel.findByIdAndUpdate(data._id, { + data = await ArticleProxy.updateById(data._id, { permalink: `${config.site}/article/${data._id}` - }, { - new : true }).exec().catch(err => { - ctx.log.error(err.message) + ctx.log.error('文章永久链接更新失败', err.message) return data }) } - ctx.success(data) + ctx.success(data, '文章创建成功') } else { - ctx.fail() + ctx.fail('文章创建失败') } } +// 文章更新 exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - const title = ctx.validateBody('title').optional().isString('the "title" parameter should be String type').val() - const content = ctx.validateBody('content').optional().isString('the "content" parameter should be String type').val() - const keywords = ctx.validateBody('keywords').optional().isArray('the "keywords" parameter should be Array type').val() - const description = ctx.validateBody('description').optional().isString('the "description" parameter should be String type').val() + const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() + const title = ctx.validateBody('title').optional().val() + const content = ctx.validateBody('content').optional().val() + const keywords = ctx.validateBody('keywords').optional().toArray().val() + const description = ctx.validateBody('description').optional().val() const category = ctx.validateBody('category').optional().isObjectId().val() const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'the "state" parameter is not the expected value').val() - const thumb = ctx.validateBody('thumb').optional().isString('the "thumb" parameter should be String type').val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数错误').val() + const thumb = ctx.validateBody('thumb').optional().val() const createdAt = ctx.validateBody('createdAt').optional().toString().val() - const article = {} - const cache = await ArticleModel.findById(id).exec().catch(err => { - ctx.log.error(err.message) - return null - }) title && (article.title = title) keywords && (article.keywords = keywords) @@ -312,60 +268,46 @@ exports.update = async (ctx, next) => { article.state = state } - if (content) { + if (content !== undefined) { article.content = content article.renderedContent = marked(content) } - const data = await ArticleModel.findByIdAndUpdate(id, article, { new: true }) - .populate('category tag') - .exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) + const data = await ArticleProxy.updateById(id, article).populate('category tag').exec() - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + data + ? ctx.success(data, '文章更新成功') + : ctx.fail('文章更新失败') } +// 删除文章 exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - const data = await ArticleModel.remove({ _id: id }).catch(err => { - ctx.log.error(err.message) - return null - }) + const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() + const data = await ArticleProxy.delById(id).exec() if (data && data.result && data.result.ok) { - ctx.success() + ctx.success(true, '文章删除成功') } else { - ctx.fail() + ctx.fail('文章删除失败') } } +// 文章点赞 exports.like = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() - - const data = await ArticleModel.findByIdAndUpdate(id, { + const data = await ArticleProxy.updateById(id, { $inc: { 'meta.ups': like ? 1 : -1 } - }).catch(err => { - ctx.log.error(err.message) - return null }) - if (data) { - ctx.success() - } else { - ctx.fail(-1, 'the article not found') - } + data + ? ctx.success(true, '文章点赞成功') + : ctx.fail('文章点赞失败') } +// 文章归档 exports.archive = async (ctx, next) => { let data = await ArticleModel.aggregate([ { $match: { state: 1 } }, @@ -419,7 +361,7 @@ exports.archive = async (ctx, next) => { ctx.success({ count, list: data || [] - }) + }, '获取文章归档成功') } /** @@ -430,11 +372,11 @@ exports.archive = async (ctx, next) => { async function getRelatedArticles (ctx, data) { data.related = [] let { _id, tag = [] } = data - const articles = await ArticleModel.find({ + const articles = await ArticleProxy.find({ _id: { $nin: [ _id ] }, state: 1, - tag: { $in: tag.map(t => t._id) }} - ) + tag: { $in: tag.map(t => t._id) } + }) .select('title thumb createdAt publishedAt meta category') .populate({ path: 'category', @@ -442,13 +384,13 @@ async function getRelatedArticles (ctx, data) { }) .exec() .catch(err => { - ctx.log.error('related articles access failed, err: ', err.message) + ctx.log.error('关联文章查询失败,err:', err.message) return null }) if (articles) { - // 取前10篇 - data.related = articles.slice(0, 10) + // 最多取前10篇 + data.related = articles.slice(0, config.limit.relatedArticleLimit) } } @@ -464,7 +406,7 @@ async function getSiblingArticles (ctx, data) { if (!ctx._isAuthenticated) { query.state = 1 } - let prev = await ArticleModel.findOne(query) + const prev = await ArticleProxy.findOne(query) .select('title createdAt publishedAt thumb category') .populate({ path: 'category', @@ -474,10 +416,10 @@ async function getSiblingArticles (ctx, data) { .lt('createdAt', data.createdAt) .exec() .catch(err => { - ctx.log.error('adjacent articles access failed, err: ', err.message) + ctx.log.error('前一篇文章获取失败,err:', err.message) return null }) - let next = await ArticleModel.findOne(query) + const next = await ArticleProxy.findOne(query) .select('title createdAt publishedAt thumb category') .populate({ path: 'category', @@ -487,11 +429,12 @@ async function getSiblingArticles (ctx, data) { .gt('createdAt', data.createdAt) .exec() .catch(err => { - ctx.log.error('adjacent articles access failed, err: ', err.message) + ctx.log.error('后一篇文章获取失败,err:', err.message) return null }) - prev = prev && prev.toObject() - next = next && next.toObject() - data.adjacent = { prev, next } + data.adjacent = { + prev: prev && prev.toObject() || null, + next: next && next.toObject() || null + } } } diff --git a/server/controller/comment.js b/server/controller/comment.js index 548c75d..c788323 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -14,7 +14,7 @@ const debug = getDebug('Comment') const isProd = process.env.NODE_ENV === 'development' exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.commentLimit).toInt().gt(0, '每页评论数量必须大于0').val() + const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.commentLimit).toInt().gt(0, '每页评论数量必须大于0').val() const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() const type = ctx.validateQuery('type').optional().toInt().isIn([0, 1], '评论类型参数无效').val() @@ -517,7 +517,7 @@ async function checkUserSpam (user) { const spamComments = userComments.filter(c => c.spam) // 如果用户以往评论中spam评论数量大于等于spam限制 - if (spamComments.length >= config.commentSpamLimit) { + if (spamComments.length >= config.limit.commentSpamLimit) { if (!user.mute) { // 将用户禁言 await UserModel.update({ _id: user._id }, { @@ -637,7 +637,7 @@ async function checkAuthor (author) { // 创建 user = await new UserModel({ ...update, - role: config.roleMap.USER + role: config.constant.roleMap.USER }) .save() .catch(err => { diff --git a/server/controller/moment.js b/server/controller/moment.js index 8b42138..c5dc77f 100644 --- a/server/controller/moment.js +++ b/server/controller/moment.js @@ -12,7 +12,7 @@ const { getDebug, getLocation } = require('../util') const debug = getDebug('Moment') exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.momentLimit).toInt().gt(0, '每页数量必须大于0').val() + const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.momentLimit).toInt().gt(0, '每页数量必须大于0').val() const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], '个人动态状态参数无效').val() const keyword = ctx.validateQuery('keyword').optional().toString().val() diff --git a/server/controller/user.js b/server/controller/user.js index 0365d80..9c7c07b 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -64,7 +64,7 @@ exports.update = async (ctx, next) => { const description = ctx.validateBody('description').optional().isString('the "description" parameter should be String type').val() const avatar = ctx.validateBody('avatar').optional().isString('the "avatar" parameter should be String type').val() const slogan = ctx.validateBody('slogan').optional().isString('the "slogan" parameter should be String type').val() - const role = ctx.validateBody('role').optional().toInt().isIn(Object.values(config.roleMap), 'the "role" parameter is not the expected value').val() + const role = ctx.validateBody('role').optional().toInt().isIn(Object.values(config.constant.roleMap), 'the "role" parameter is not the expected value').val() const password = ctx.validateBody('password').optional().isString('the "password" parameter should be String type').val() const mute = ctx.validateBody('mute').optional().toBoolean().val() const user = {} @@ -162,7 +162,7 @@ exports.guests = async (ctx, next) => { _id: item._id, $nor: [ { - role: config.roleMap.ADMIN + role: config.constant.roleMap.ADMIN }, { 'github.login': config.author } @@ -185,7 +185,7 @@ exports.updateGithubInfo = async () => { return [] }) const githubUsers = users.reduce((sum, user) => { - if (user.role === config.roleMap.GITHUB_USER || (user.role === config.roleMap.ADMIN && user.github.login)) { + if (user.role === config.constant.roleMap.GITHUB_USER || (user.role === config.constant.roleMap.ADMIN && user.github.login)) { sum.push(user) } return sum @@ -193,7 +193,6 @@ exports.updateGithubInfo = async () => { const updates = await getGithubUsersInfo(githubUsers.map(user => user.github.login)) Promise.all( updates.reduce((tasks, data, index) => { - console.log(data) const user = githubUsers[index] const u = { name: data.name, diff --git a/server/middleware/error.js b/server/middleware/error.js index e067b40..486b75d 100644 --- a/server/middleware/error.js +++ b/server/middleware/error.js @@ -10,17 +10,17 @@ module.exports = async (ctx, next) => { try { await next() } catch (err) { - // TODO: 错误日志钩子 let code = err.status || 500 - + if (err.name === 'ValidationError') { code = 10001 } - + ctx.fail(code, err.message) ctx.status = 200 - + if (code === 500) { + // TODO: 错误日志记录 ctx.log.error( { req: ctx.req, err }, ' --> %s %s %d', diff --git a/server/middleware/header.js b/server/middleware/header.js index 9af9ed1..1a13e83 100644 --- a/server/middleware/header.js +++ b/server/middleware/header.js @@ -10,7 +10,7 @@ const config = require('../config') module.exports = async (ctx, next) => { const { request, response } = ctx - const allowedOrigins = config.auth.allowedOrigins + const allowedOrigins = config.constant.allowedOrigins const origin = request.get('origin') || '' const allowed = request.query._DEV_ || origin.includes('localhost') diff --git a/server/middleware/response.js b/server/middleware/response.js index 5768475..1736744 100644 --- a/server/middleware/response.js +++ b/server/middleware/response.js @@ -8,9 +8,11 @@ const config = require('../config') const { isType } = require('../util') +const successMsg = config.constant.codeMap['200'] +const failMsg = config.constant.codeMap['-1'] module.exports = async (ctx, next) => { - ctx.success = (data = null, message = config.codeMap[200]) => { + ctx.success = (data = null, message = successMsg) => { ctx.status = 200 ctx.body = { code: 200, @@ -30,7 +32,7 @@ module.exports = async (ctx, next) => { ctx.body = { code, success: false, - message: message || config.codeMap[code] || config.codeMap['-1'], + message: message || config.constant.codeMap[code] || failMsg, data } } diff --git a/server/plugins/mongo.js b/server/plugins/mongo.js index cf6b515..e637a6f 100644 --- a/server/plugins/mongo.js +++ b/server/plugins/mongo.js @@ -48,7 +48,7 @@ async function seedOption () { // 管理员初始化 async function seedAdmin () { const admin = await UserModel.findOne({ - role: config.roleMap.ADMIN, + role: config.constant.roleMap.ADMIN, 'github.login': config.author }).exec() .catch(err => debug.error('初始化管理员查询失败,错误:', err.message)) @@ -65,7 +65,7 @@ async function createAdmin () { } data = data[0] const result = await new UserModel({ - role: config.roleMap.ADMIN, + role: config.constant.roleMap.ADMIN, name: data.name, email: data.email, password: bhash(config.auth.defaultPassword), diff --git a/server/plugins/validation.js b/server/plugins/validation.js index ea592c2..f6d01b1 100644 --- a/server/plugins/validation.js +++ b/server/plugins/validation.js @@ -11,9 +11,9 @@ const Validator = require('koa-bouncer').Validator const { isObjectId } = require('../util') Validator.addMethod('notEmpty', function (tip) { - this.isString(`the "${this.key}" parameter should be String type`) + this.isString(`${this.key}参数格式错误,期望格式:String`) if (this.val().length === 0) { - this.throwError(tip || `the "${this.key}" parameter should not be empty value`) + this.throwError(tip || `${this.key}参数不能为空`) } return this }) @@ -23,7 +23,7 @@ Validator.addMethod('isObjectId', function (tip) { if (val !== undefined) { this.toString() if (!mongoose.Types.ObjectId.isValid(val)) { - this.throwError(tip || `the "${this.key}" parameter should be ObjectId type`) + this.throwError(tip || `${this.key}参数格式错误,期望格式:ObjectId`) } } return this @@ -35,7 +35,7 @@ Validator.addMethod('isObjectIdArray', function (tip) { this.isArray() val.forEach(data => { if (!isObjectId(data)) { - this.throwError(tip || `the "${this.key}" parameter contains a data(${data}) that is not ObjectId type`) + this.throwError(tip || `${this.key}参数格式错误,期望格式:[ObjectId]`) } }) } diff --git a/server/proxy/article.js b/server/proxy/article.js new file mode 100644 index 0000000..6eafc00 --- /dev/null +++ b/server/proxy/article.js @@ -0,0 +1,71 @@ +/** + * @desc Article model proxy + * @author Jooger + * @date 26 Jan 2018 + */ + +'use strict' + +const { ArticleModel } = require('../model') + +module.exports = class ArticleProxy { + static newAndSave (docs) { + if (!Array.isArray(docs)) { + docs = [docs] + } + return ArticleModel.insertMany(docs) + } + + static paginate (query, opt = {}) { + return ArticleModel.paginate(query, opt) + } + + static getById (articleId) { + return ArticleModel.findById(articleId) + } + + static find (query = {}, opt = {}) { + return ArticleModel.find(query, null, opts) + } + + static findOne (query = {}, opt = {}) { + return ArticleModel.findOne(query, null, opt) + } + + static updateById (id, doc, opt = {}) { + return ArticleModel.findByIdAndUpdate(id, doc, { + new: true, + ...opt + }) + } + + static updateOne (query = {}, doc = {}, opt = {}) { + return ArticleModel.findOneAndUpdate(query, doc, { + new: true, + ...opt + }) + } + + static updateMany (query = {}, doc = {}, opt = {}) { + return ArticleModel.update(query, doc, { + multi: true, + ...opt + }) + } + + static del (query) { + return ArticleModel.remove(query) + } + + static delById (articleId) { + return this.del({ _id: articleId }) + } + + static delByIds (articleIds) { + return this.del({ + _id: { + $in: articleIds + } + }) + } +} diff --git a/server/proxy/index.js b/server/proxy/index.js new file mode 100644 index 0000000..8a02546 --- /dev/null +++ b/server/proxy/index.js @@ -0,0 +1,11 @@ +/** + * @desc Model proxy entrance + * @author Jooger + * @date 26 Jan 2018 + */ + +'use strict' + +module.exports = { + ArticleProxy: require('./article') +} diff --git a/server/routes/index.js b/server/routes/index.js index 9315331..5583d5f 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -16,7 +16,7 @@ module.exports = app => { router.use('*', header) router.get('/', async (ctx, next) => { - ctx.log.error('Got a request from %s for %s', ctx.request.ip, ctx.path) + ctx.log.info('Got a root request from %s for %s', ctx.request.ip, ctx.path) ctx.body = { name: config.name, version: config.version, diff --git a/server/service/crontab.js b/server/service/crontab.js index 39bcbf8..73b0ca0 100644 --- a/server/service/crontab.js +++ b/server/service/crontab.js @@ -10,9 +10,9 @@ exports.start = () => { const { option, user } = require('../controller') // 友链 每1小时更新一次 option.updateOptionLinks() - setInterval(option.updateOptionLinks.bind(option), 1000 * 60 * 60 * 1) + setInterval(() => option.updateOptionLinks(), 1000 * 60 * 60 * 1) // 用户 每1天更新一次 user.updateGithubInfo() - setInterval(user.updateGithubInfo.bind(user), 1000 * 60 * 60 * 24) + setInterval(() => user.updateGithubInfo(), 1000 * 60 * 60 * 24) } diff --git a/server/service/netease-music.js b/server/service/netease-music.js index c1c2352..8fed020 100644 --- a/server/service/netease-music.js +++ b/server/service/netease-music.js @@ -1,5 +1,5 @@ /** - * @desc 网易云音乐 TEST (暂未使用) + * @desc 网易云音乐 TEST (备用) * @author Jooger * @date 30 Sep 2017 */ diff --git a/server/util/debug.js b/server/util/debug.js index f430bb8..6cb8b5d 100644 --- a/server/util/debug.js +++ b/server/util/debug.js @@ -40,7 +40,7 @@ module.exports = function getDebug (namespace = '') { deBug.enabled = true deBug.color = levelMap[key].level const args = slice.call(arguments) - args[0] = levelMap[key].emoji + ' ' + args[0] + // args[0] = levelMap[key].emoji + ' ' + args[0] deBug.apply(null, args) } }) diff --git a/server/util/encrypt.js b/server/util/encrypt.js index 5660a4f..e7e6f06 100644 --- a/server/util/encrypt.js +++ b/server/util/encrypt.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc Netease service encrypt * @author Jooger * @date 30 Sep 2017 */ diff --git a/server/util/index.js b/server/util/index.js index 5c96dbc..d745038 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -9,6 +9,7 @@ const bcrypt = require('bcryptjs') const mongoose = require('mongoose') const validator = require('validator') +const config = require('../config') exports.getDebug = require('./debug') @@ -63,21 +64,7 @@ exports.randomString = (length = 8) => { return id } -const monthMap = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' -] -exports.getMonthFromNum = (num = 1) => monthMap[num - 1] || '' +exports.getMonthFromNum = (num = 1) => config.constant.monthMap[num - 1] || '' Object.keys(validator).forEach(key => { exports[key] = function () { @@ -89,3 +76,16 @@ exports.isSiteUrl = (site = '') => validator.isURL(site, { protocols: ['http', 'https'], require_protocol: true }) + +// 获取分页请求的响应数据 +exports.getDocsPaginationData = (docs = {}) => { + return { + list: docs.docs, + pagination: { + total: docs.total, + current_page: docs.page > docs.pages ? docs.pages : docs.page, + total_page: docs.pages, + per_page: docs.limit + } + } +} From 386266743d61db1300f8992da0d505f4689642f4 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 27 Jan 2018 02:51:07 +0800 Subject: [PATCH 090/208] [update] update README and package.json --- README.md | 23 ++++++++++++++++------- package.json | 10 +++++----- server/controller/article.js | 2 +- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5903b96..222d946 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -[![GitHub forks](https://img.shields.io/github/forks/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/network) -[![GitHub stars](https://img.shields.io/github/stars/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/stargazers) -[![GitHub issues](https://img.shields.io/github/issues/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/issues) -[![GitHub last commit](https://img.shields.io/github/last-commit/jo0ger/jooger.me-server.svg?style=flat-square)](https://github.com/jo0ger/jooger.me-server/commits/master) +[![GitHub forks](https://img.shields.io/github/forks/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/network) +[![GitHub stars](https://img.shields.io/github/stars/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/stargazers) +[![GitHub issues](https://img.shields.io/github/issues/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/issues) +[![GitHub last commit](https://img.shields.io/github/last-commit/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/commits/master) -## jooger.me-server +## node-server ⚡️ My blog's api server build with koa2 and mongoose,a RESTful application. @@ -11,7 +11,7 @@ * jooger.me: [https://jooger.me](https://jooger.me) -* jooger.me-server: [https://api.jooger.me](https://api.jooger.me) +* node-server: [https://api.jooger.me](https://api.jooger.me) * jooger.me-admin: [https://admin.jooger.me](https://admin.jooger.me) @@ -43,7 +43,7 @@ $ npm run test ## Directory tree ``` -jooger.me-server +node-server |____api.md // api文档(待完善) |____bin // 启动目录 |____ecosystem.config.js // pm2启动文件,需要自己手动创建 @@ -64,6 +64,7 @@ jooger.me-server | | |____mongo.js // MongoDB驱动(mongoose) | | |____redis.js // Redis | | |____validation.js // 额外的校验规则 +| | |____gc.js // GC | |____routes // 路由目录 | | |____backend.js // 后台路由 | | |____frontend.js // 前台路由 @@ -71,7 +72,9 @@ jooger.me-server | | |____crontab.js // 定时更新任务 | | |____github-passport.js // Github验证 | | |____github-userinfo.js // 获取Github用户信息 +| | |____github-token.js // 获取Github登录token | | |____netease-music.js // 网易云音乐api +| |____proxy // model操作代理 | |____util // 常用工具 |____test // 测试目录 @@ -103,6 +106,10 @@ jooger.me-server * ~~文章归档api~~(2018.01.04) +* model代理 + +* TypeScript升级 + * 邮件模板 * 消息api @@ -112,3 +119,5 @@ jooger.me-server * 统计api * 完善API文档 + +* 测试case diff --git a/package.json b/package.json index 0ee6383..cc732d4 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "jooger.me-server", - "version": "1.3.0", + "name": "node-server", + "version": "1.4.0", "private": true, "description": "🔥 My blog's api server build by koa2 and mongoose", - "homepage": "https://github.com/jo0ger/jooger.me-server", + "homepage": "https://github.com/jo0ger/node-server", "author": { "name": "jo0ger", "email": "iamjooger@gmail.com", @@ -11,7 +11,7 @@ }, "repository": { "type": "https", - "url": "https://github.com/jo0ger/jooger.me-server.git" + "url": "https://github.com/jo0ger/node-server.git" }, "keywords": [ "jooger.me", @@ -23,7 +23,7 @@ ], "license": "MIT", "bugs": { - "url": "https://github.com/jo0ger/jooger.me-server/issues" + "url": "https://github.com/jo0ger/node-server/issues" }, "bin": "./node_modules/.bin/", "scripts": { diff --git a/server/controller/article.js b/server/controller/article.js index 95bf80b..ab7318d 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -9,7 +9,7 @@ const config = require('../config') const { ArticleProxy } = require('../proxy') const { ArticleModel, CategoryModel, TagModel } = require('../model') -const { marked, isObjectId, createObjectId, getDebug, getMonthFromNum, getDocsPagination } = require('../util') +const { marked, isObjectId, createObjectId, getDebug, getMonthFromNum, getDocsPaginationData } = require('../util') const debug = getDebug('Article') // 文章列表 From a5388af020f812484910e1bfd98d0aca9a784f0d Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 28 Jan 2018 01:51:48 +0800 Subject: [PATCH 091/208] [update] update model proxy and controller --- README.md | 2 + server/app.js | 31 ++--- server/config/index.js | 5 +- server/controller/article.js | 47 +++---- server/controller/auth.js | 2 +- server/controller/category.js | 157 ++++++++-------------- server/controller/moment.js | 2 +- server/controller/music.js | 101 ++------------ server/controller/option.js | 86 ++---------- server/controller/tag.js | 130 ++++++------------ server/controller/user.js | 213 +++++++++++------------------- server/middleware/authenticate.js | 6 +- server/model/schema/user.js | 3 +- server/plugins/crontab.js | 21 +++ server/plugins/gc.js | 17 ++- server/plugins/index.js | 1 + server/proxy/article.js | 65 +-------- server/proxy/base.js | 77 +++++++++++ server/proxy/category.js | 18 +++ server/proxy/comment.js | 18 +++ server/proxy/index.js | 8 +- server/proxy/moment.js | 18 +++ server/proxy/option.js | 18 +++ server/proxy/tag.js | 18 +++ server/proxy/user.js | 18 +++ server/routes/backend.js | 7 +- server/routes/frontend.js | 2 +- server/service/crontab.js | 18 --- server/service/github-userinfo.js | 4 +- server/service/index.js | 4 +- server/service/model-update.js | 189 ++++++++++++++++++++++++++ server/service/netease-music.js | 7 +- 32 files changed, 673 insertions(+), 640 deletions(-) create mode 100644 server/plugins/crontab.js create mode 100644 server/proxy/base.js create mode 100644 server/proxy/category.js create mode 100644 server/proxy/comment.js create mode 100644 server/proxy/moment.js create mode 100644 server/proxy/option.js create mode 100644 server/proxy/tag.js create mode 100644 server/proxy/user.js delete mode 100644 server/service/crontab.js create mode 100644 server/service/model-update.js diff --git a/README.md b/README.md index 222d946..87afb36 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ node-server * TypeScript升级 +* ESlint + * 邮件模板 * 消息api diff --git a/server/app.js b/server/app.js index 4b02e61..099279e 100644 --- a/server/app.js +++ b/server/app.js @@ -1,5 +1,5 @@ /** - * @desc Server entry + * @desc Server entrance * @author Jooger * @date 25 Sep 2017 */ @@ -16,21 +16,14 @@ const passport = require('koa-passport') const compress = require('koa-compress') const bodyparser = require('koa-bodyparser') const koaBunyanLogger = require('koa-bunyan-logger') - const packageInfo = require('../package.json') const middlewares = require('./middleware') +const routes = require('./routes') const config = require('./config') -const { mongo, redis, akismet, validation, mailer, gc } = require('./plugins') -const { crontab } = require('./service') -const isProd = process.env.NODE_ENV === 'production' +const { mongo, redis, akismet, validation, mailer, gc, crontab } = require('./plugins') const app = new Koa() - -// connect mongodb -mongo.connect() - -// connect redis -redis.connect() +app.keys = config.auth.secrets // load custom validations bouncer.Validator = validation @@ -38,7 +31,6 @@ bouncer.Validator = validation // error handler onerror(app) -app.keys = config.auth.secrets // middlewares app.use(bodyparser({ @@ -63,8 +55,13 @@ app.use(passport.initialize()) app.use(compress()) // routes -require('./routes')(app) +routes(app) + +// connect mongodb +mongo.connect() +// connect redis +redis.connect() // akismet akismet.start() @@ -74,11 +71,7 @@ mailer.start() // crontab crontab.start() -if (isProd) { - // v8 gc - gc.start() -} - -app.on('error', function () {}) +// v8 gc +gc.start() module.exports = app diff --git a/server/config/index.js b/server/config/index.js index e50db79..195a4d6 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -90,7 +90,10 @@ const baseConfig = { USER: 1, GITHUB_USER: 2 }, - monthMap: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] + monthMap: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + redisCacheKey: { + music: 'music-data' + } } } diff --git a/server/controller/article.js b/server/controller/article.js index ab7318d..ba590a6 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -7,8 +7,7 @@ 'use strict' const config = require('../config') -const { ArticleProxy } = require('../proxy') -const { ArticleModel, CategoryModel, TagModel } = require('../model') +const { articleProxy, categoryProxy, tagProxy } = require('../proxy') const { marked, isObjectId, createObjectId, getDebug, getMonthFromNum, getDocsPaginationData } = require('../util') const debug = getDebug('Article') @@ -76,7 +75,7 @@ exports.list = async (ctx, next) => { query.category = category } else { // 普通字符串,需要先查到id - const c = await CategoryModel.findOne({ name: category }).exec() + const c = await categoryProxy.findOne({ name: category }).exec() .catch(err => { ctx.log.error(err.message) return null @@ -92,7 +91,7 @@ exports.list = async (ctx, next) => { query.tag = tag } else { // 普通字符串,需要先查到id - const t = await TagModel.findOne({ name: tag }).exec() + const t = await tagProxy.findOne({ name: tag }).exec() .catch(err => { ctx.log.error(err.message) return null @@ -131,7 +130,7 @@ exports.list = async (ctx, next) => { } } - const articles = await ArticleProxy.paginate(query, options) + const articles = await articleProxy.paginate(query, options) articles ? ctx.success(getDocsPaginationData(articles), '文章列表获取成功') @@ -141,7 +140,7 @@ exports.list = async (ctx, next) => { // 热门文章 exports.hot = async (ctx, next) => { const limit = ctx.validateQuery('limit').defaultTo(config.limit.hotLimit).toInt().gt(0, 'limit参数必须大于0').val() - const data = await ArticleProxy.find() + const data = await articleProxy.find() .sort('-meta.comments -meta.ups -meta.pvs') .select('-content -renderedContent -state') .populate([ @@ -167,9 +166,9 @@ exports.item = async (ctx, next) => { let query = null // 只有前台博客访问文章的时候pv才+1 if (!ctx._isAuthenticated) { - query = ArticleProxy.updateOne({ _id: id, state: 1 }, { $inc: { 'meta.pvs': 1 } }).select('-content') + query = articleProxy.updateOne({ _id: id, state: 1 }, { $inc: { 'meta.pvs': 1 } }).select('-content') } else { - query = ArticleProxy.getById(id) + query = articleProxy.getById(id) } data = await query.populate([ @@ -223,13 +222,13 @@ exports.create = async (ctx, next) => { article.content = content article.renderedContent = marked(content) - let data = await ArticleProxy.newAndSave(article) + let data = await articleProxy.newAndSave(article) if (data && data.length) { data = data[0] if (!data.permalink) { // 更新永久链接 - data = await ArticleProxy.updateById(data._id, { + data = await articleProxy.updateById(data._id, { permalink: `${config.site}/article/${data._id}` }).exec().catch(err => { ctx.log.error('文章永久链接更新失败', err.message) @@ -273,7 +272,7 @@ exports.update = async (ctx, next) => { article.renderedContent = marked(content) } - const data = await ArticleProxy.updateById(id, article).populate('category tag').exec() + const data = await articleProxy.updateById(id, article).populate('category tag').exec() data ? ctx.success(data, '文章更新成功') @@ -283,33 +282,31 @@ exports.update = async (ctx, next) => { // 删除文章 exports.delete = async (ctx, next) => { const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() - const data = await ArticleProxy.delById(id).exec() + const data = await articleProxy.delById(id).exec() - if (data && data.result && data.result.ok) { - ctx.success(true, '文章删除成功') - } else { - ctx.fail('文章删除失败') - } + data && data.result && data.result.ok + ? ctx.success(null, '文章删除成功') + : ctx.fail('文章删除失败') } // 文章点赞 exports.like = async (ctx, next) => { const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() - const data = await ArticleProxy.updateById(id, { + const data = await articleProxy.updateById(id, { $inc: { 'meta.ups': like ? 1 : -1 } - }) + }).exec() data - ? ctx.success(true, '文章点赞成功') + ? ctx.success(null, '文章点赞成功') : ctx.fail('文章点赞失败') } // 文章归档 -exports.archive = async (ctx, next) => { - let data = await ArticleModel.aggregate([ +exports.archives = async (ctx, next) => { + let data = await articleProxy.aggregate([ { $match: { state: 1 } }, { $sort: { createdAt: 1 } }, { @@ -372,7 +369,7 @@ exports.archive = async (ctx, next) => { async function getRelatedArticles (ctx, data) { data.related = [] let { _id, tag = [] } = data - const articles = await ArticleProxy.find({ + const articles = await articleProxy.find({ _id: { $nin: [ _id ] }, state: 1, tag: { $in: tag.map(t => t._id) } @@ -406,7 +403,7 @@ async function getSiblingArticles (ctx, data) { if (!ctx._isAuthenticated) { query.state = 1 } - const prev = await ArticleProxy.findOne(query) + const prev = await articleProxy.findOne(query) .select('title createdAt publishedAt thumb category') .populate({ path: 'category', @@ -419,7 +416,7 @@ async function getSiblingArticles (ctx, data) { ctx.log.error('前一篇文章获取失败,err:', err.message) return null }) - const next = await ArticleProxy.findOne(query) + const next = await articleProxy.findOne(query) .select('title createdAt publishedAt thumb category') .populate({ path: 'category', diff --git a/server/controller/auth.js b/server/controller/auth.js index 7635430..d0846c5 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -12,8 +12,8 @@ const config = require('../config') const { UserModel } = require('../model') const { bhash, bcompare, getDebug, signToken, proxy, randomString } = require('../util') const debug = getDebug('Auth') -const debugGithub = getDebug('Github:Auth') const { getGithubToken, getGithubAuthUserInfo } = require('../service') +const debugGithub = getDebug('Github:Auth') const isProd = process.env.NODE_ENV === 'production' exports.localLogin = async (ctx, next) => { diff --git a/server/controller/category.js b/server/controller/category.js index bace315..3e24761 100644 --- a/server/controller/category.js +++ b/server/controller/category.js @@ -6,11 +6,13 @@ 'use strict' -const { CategoryModel, ArticleModel } = require('../model') +const { articleProxy, categoryProxy } = require('../proxy') +// 分类列表 exports.list = async (ctx, next) => { const keyword = ctx.validateQuery('keyword').optional().toString().val() - const rank = ctx.validateQuery('rank').defaultTo(1).toInt().isIn([0, 1], 'the "rank" parameter is not the expected value').val() + // 是否按照list属性排序 + const rank = ctx.validateQuery('rank').defaultTo(1).toInt().isIn([0, 1], 'rank参数错误').val() const query = {} // 搜索关键词 @@ -26,40 +28,35 @@ exports.list = async (ctx, next) => { sort = 'list ' + sort } - const data = await CategoryModel.find(query).sort(sort).catch(err => { - ctx.log.error(err.message) - return null - }) + const data = await categoryProxy.find(query).sort(sort) if (data) { for (let i = 0; i < data.length; i++) { - if (data[i].toObject) { + if (typeof data[i].toObject === 'function') { data[i] = data[i].toObject() } - const articles = await ArticleModel.find({ category: data[i]._id }).exec().catch(err => { + const articles = await articleProxy.find({ category: data[i]._id }).exec().catch(err => { ctx.log.error(err.message) return [] }) data[i].count = articles.length } - ctx.success(data) + ctx.success(data, '分类列表获取成功') } else { - ctx.fail() + ctx.fail('分类列表获取失败') } } +// 分类详情 exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() - let data = await CategoryModel.findById(id).exec().catch(err => { - ctx.log.error(err.message) - return null - }) + let data = await categoryProxy.getById(id).exec() if (data) { data = data.toObject() - const articles = await ArticleModel.find({ tag: id }) - .select('-tag') + const articles = await articleProxy.find({ category: id }) + .select('-category') .exec() .catch(err => { ctx.log.error(err.message) @@ -67,119 +64,71 @@ exports.item = async (ctx, next) => { }) data.articles = articles data.articles_count = articles.length - ctx.success(data) + ctx.success(data, '分类详情获取成功') } else { - ctx.fail() + ctx.fail('分类详情获取失败') } } +// 分类创建 exports.create = async (ctx, next) => { - const name = ctx.validateBody('name') - .required('the "name" parameter is required') - .notEmpty() - .isString('the "name" parameter should be String type') - .val() - const description = ctx.validateBody('description') - .optional() - .isString('the "description" parameter should be String type') - .val() - const list = ctx.validateBody('list') - .defaultTo(1) - .toInt() - .val() - const ext = ctx.validateBody('extends') - .optional() - .isArray('the "extends" parameter should be Array type') - .val() - - const { length } = await CategoryModel.find({ name }).exec().catch(err => { + const name = ctx.validateBody('name').required('缺少分类名称').notEmpty().val() + const description = ctx.validateBody('description').optional().val() + const list = ctx.validateBody('list').defaultTo(1).toInt().val() + const ext = ctx.validateBody('extends').optional().toArray().val() + + const { length } = await categoryProxy.find({ name }).exec().catch(err => { ctx.log.error(err.message) return [] }) if (!length) { - const data = await new CategoryModel({ + const data = await categoryProxy.newAndSave({ name, description, extends: ext, list - }).save().catch(err => { - ctx.log.error(err.message) - return null }) - if (data) { - return ctx.success(data) - } else { - ctx.fail() - } + data && data.length + ? ctx.success(data, '分类创建成功') + : ctx.fail('分类创建失败') } else { - ctx.fail(-1, `the tag(${name}) is already exist`) + ctx.fail(`【${name}】分类已经存在`) } } +// 分类更新 exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - const name = ctx.validateBody('name') - .optional() - .isString('the "name" parameter should be String type') - .val() - const description = ctx.validateBody('description') - .optional() - .isString('the "description" parameter should be String type') - .val() - const list = ctx.validateBody('list') - .optional() - .toInt() - .val() - - const tag = {} - - name && (tag.name = name) - description && (tag.description = description) - list && (tag.list = list) - - const data = await CategoryModel.findByIdAndUpdate(id, tag, { - new: true - }).catch(err => { - ctx.log.error(err.message) - return null - }) + const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() + const name = ctx.validateBody('name').optional().val() + const description = ctx.validateBody('description').optional().val() + const list = ctx.validateBody('list').optional().toInt().val() + const category = {} - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + name && (category.name = name) + description && (category.description = description) + list && (category.list = list) + + const data = await categoryProxy.updateById(id, category).exec() + + data + ? ctx.success(data, '分类更新成功') + : ctx.fail('分类更新失败') } +// 删除分类 exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - const data = await CategoryModel.remove({ _id: id }).catch(err => { - ctx.log.error(err.message) - return null - }) + const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() + const articles = await articleProxy.find({ category: id }).exec() - if (data && data.result && data.result.ok) { - // 删除所有文章关联关系 - const articles = await ArticleModel.find({ tag: data._id }) - .exec() - .catch(err => { - ctx.log.error(err.message) - return [] - }) - // TODO: 这里应该需要一个容错机制,保证如果有一篇文章没有删除成功的话,需要在规定次数内反复删除 - await Promise.all( - articles.map(item => { - return ArticleModel.findByIdAndUpdate(item._id, { - tag: item.tag.filter(tag => tag.toString() !== data._id.toString()) - }).exec() - }) - ).catch(err => { - ctx.log.error(err.message) - }) - ctx.success() + if (articles && articles.length) { + // 分类下面有文章,不能删除 + ctx.fail('该分类下有文章,不能删除') } else { - ctx.fail() + const data = await categoryProxy.delById(id).exec() + data && data.result && data.result.ok + ? ctx.success(null, '分类删除成功') + : ctx.fail('分类删除失败') } } diff --git a/server/controller/moment.js b/server/controller/moment.js index c5dc77f..401877e 100644 --- a/server/controller/moment.js +++ b/server/controller/moment.js @@ -14,7 +14,7 @@ const debug = getDebug('Moment') exports.list = async (ctx, next) => { const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.momentLimit).toInt().gt(0, '每页数量必须大于0').val() const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() - const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], '个人动态状态参数无效').val() + const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'state参数错误').val() const keyword = ctx.validateQuery('keyword').optional().toString().val() const query = {} diff --git a/server/controller/music.js b/server/controller/music.js index 770934d..83a9706 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -6,18 +6,13 @@ 'use strict' -const NeteseMusic = require('simple-netease-cloud-music') const config = require('../config') -const { fetchNE } = require('../service') -const { OptionModel } = require('../model') -const { proxy, getDebug, isType } = require('../util') +const { modelUpdate, netease } = require('../service') +const { optionProxy } = require('../proxy') +const { proxy, getDebug } = require('../util') const { redis } = require('../plugins') -const expired = 60 * 10 // 过期时间10分钟 - const isProd = process.env.NODE_ENV === 'production' const debug = getDebug('Music') -const neteaseMusic = new NeteseMusic() -const cacheKey = 'music-data' exports.list = async (ctx, next) => { // 后台实时获取 @@ -28,10 +23,10 @@ exports.list = async (ctx, next) => { .isString('the "play_list_id" parameter should be String type') .val() - const data = await fetchSonglist(playListId) + const data = await modelUpdate.fetchSonglist(playListId) ctx.success(data) } else { - const option = await OptionModel.findOne({}).exec().catch(err => { + const option = await optionProxy.findOne().exec().catch(err => { ctx.log.error(err.message) return null }) @@ -41,7 +36,7 @@ exports.list = async (ctx, next) => { } const playListId = option.musicId - const musicData = await redis.get(cacheKey) + const musicData = await redis.get(config.constant.redisCacheKey.music) // hit if (musicData && musicData.id === playListId) { @@ -49,7 +44,7 @@ exports.list = async (ctx, next) => { } // update cache - const data = await exports.updateMusicCache(playListId) + const data = await modelUpdate.updateMusicCache(playListId) ctx.success(data && data.data || {}) } } @@ -61,7 +56,7 @@ exports.item = async (ctx, next) => { .isString('the "song_id" parameter should be String type') .val() - const { songs } = await neteaseMusic.song(songId) + const { songs } = await netease.neteaseMusic.song(songId) ctx.success(songs) } @@ -73,9 +68,7 @@ exports.url = async (ctx, next) => { .isString('the "song_id" parameter should be String type') .val() - // BUG: 库出错了,暂时不用此库请求URL - // const data = await neteaseMusic.url(songId).then(data => { - const data = await fetchNE('songUrl', songId).then(data => { + const data = await netease.neteaseMusic.url(songId).then(data => { if (!isProd) { return data.data || [] } @@ -114,79 +107,3 @@ exports.cover = async (ctx, next) => { ctx.success(data) } - -// 获取除了歌曲链接和歌词外其他信息 -function fetchSonglist (playListId) { - return neteaseMusic._playlist(playListId).then(({ playlist }) => { - if (!playlist) { - return null - } - const tracks = playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { - return { - id, - name, - duration: dt || 0, - album: al && { - name: al.name, - cover: isProd ? (proxy(al.picUrl) || '') : al.picUrl, - tns: al.tns - } || {}, - artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], - tns: tns || [] - } - }) - return { - id: playListId, - tracks, - name: playlist.name, - description: playlist.description, - tags: playlist.tags - } - }).catch(err => { - debug.error('歌单列表获取失败,错误:', err.message) - return null - }) -} - -// 更新song list cache -let lock = false -exports.updateMusicCache = async function (playListId = '') { - if (lock) { - debug.warn('缓存更新中...') - return redis.get(cacheKey) || null - } - lock = true - if (!playListId) { - const option = await OptionModel.findOne({}).exec().catch(err => { - debug.error('Option查找失败,错误:', err.message) - return null - }) - - if (!option || !option.musicId) { - debug.warn('歌单ID未配置') - lock = false - return redis.get(cacheKey) || null - } - playListId = option.musicId - } - - const data = await fetchSonglist(playListId) - if (!data) { - lock = false - return redis.get(cacheKey) || null - } - const set = { - id: playListId, - data - } - - // 设置10分钟过期 - redis.set(cacheKey, set, expired).then(() => { - debug.success('缓存更新成功,歌单ID:', playListId) - }).catch(err => { - debug.error('缓存更新失败,歌单ID:%s,错误:%s', playListId, err.message) - }) - - lock = false - return set -} diff --git a/server/controller/option.js b/server/controller/option.js index f39d5f6..1640fdf 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -6,87 +6,25 @@ 'use strict' -const { OptionModel } = require('../model') -const { getGithubUsersInfo } = require('../service') +const { optionProxy } = require('../proxy') const { updateMusicCache } = require('./music') const { getDebug, proxy } = require('../util') +const { modelUpdate } = require('../service') const debug = getDebug('Option') +// 站点参数数据 exports.data = async (ctx, next) => { - const data = await OptionModel.findOne().exec().catch(err => { - debug.error('查找失败,错误:', err.message) - ctx.log.error(err.message) - return null - }) - - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + const data = await optionProxy.findOne().exec() + data + ? ctx.success(data, '站点参数获取成功') + : ctx.fail('站点参数获取失败') } +// 站点参数更新 exports.update = async (ctx, next) => { const option = ctx.request.body - - const data = await exports.updateOptionLinks(option) - - if (data) { - ctx.success(data) - } else { - ctx.fail() - } -} - -// update lock -let lock = false -exports.updateOptionLinks = async function (option = null) { - if (lock) { - debug.warn('友链更新中...') - return - } - lock = true - if (!option) { - option = await OptionModel.findOne().exec().catch(err => { - debug.error('数据查找失败,错误:', err.message) - ctx.log.error(err.message) - return {} - }) - } - - // 更新友链 - option.links = await generateLinks(option.links) - - const data = await OptionModel.findOneAndUpdate({}, option, { new: true }).exec().catch(err => { - debug.error('数据更新失败,错误:', err.message) - ctx.log.error(err.message) - return null - }) - - if (data) { - debug.success('友链更新成功') - } - lock = false - return data -} - -// 更新友链 -async function generateLinks (links = []) { - if (links && links.length) { - const githubNames = links.map(link => link.github) - const usersInfo = await getGithubUsersInfo(githubNames) - - if (usersInfo) { - return links.map((link, index) => { - const userInfo = usersInfo[index] - if (userInfo) { - link.avatar = proxy(userInfo.avatar_url) - link.slogan = userInfo.bio - link.site = link.site || userInfo.blog || userInfo.url - } - return link - }) - } - } - return links + const data = await modelUpdate.updateOption(option) + data + ? ctx.success(data, '站点参数更新成功') + : ctx.fail('站点参数更新失败') } diff --git a/server/controller/tag.js b/server/controller/tag.js index 6ae5f0d..27c1d46 100644 --- a/server/controller/tag.js +++ b/server/controller/tag.js @@ -6,8 +6,9 @@ 'use strict' -const { TagModel, ArticleModel } = require('../model') +const { tagProxy, articleProxy } = require('../proxy') +// 标签列表 exports.list = async (ctx, next) => { const keyword = ctx.validateQuery('keyword').optional().toString().val() @@ -20,39 +21,34 @@ exports.list = async (ctx, next) => { ] } - const data = await TagModel.find(query).sort('-createdAt').catch(err => { - ctx.log.error(err.message) - return null - }) + const data = await tagProxy.find(query).sort('-createdAt') if (data) { for (let i = 0; i < data.length; i++) { - if (data[i].toObject) { + if (typeof data[i].toObject === 'function') { data[i] = data[i].toObject() } - const articles = await ArticleModel.find({ tag: data[i]._id }).exec().catch(err => { + const articles = await articleProxy.find({ tag: data[i]._id }).exec().catch(err => { ctx.log.error(err.message) return [] }) data[i].count = articles.length } - ctx.success(data) + ctx.success(data, '标签列表获取成功') } else { - ctx.fail() + ctx.fail('标签列表获取失败') } } +// 标签详情 exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() - let data = await TagModel.findById(id).exec().catch(err => { - ctx.log.error(err.message) - return null - }) + let data = await tagProxy.getById(id).exec() if (data) { data = data.toObject() - const articles = await ArticleModel.find({ tag: id }) + const articles = await articleProxy.find({ tag: id }) .select('-tag') .exec() .catch(err => { @@ -61,109 +57,67 @@ exports.item = async (ctx, next) => { }) data.articles = articles data.articles_count = articles.length - ctx.success(data) + ctx.success(data, '标签详情获取成功') } else { - ctx.fail() + ctx.fail('标签详情获取失败') } } +// 标签创建 exports.create = async (ctx, next) => { - const name = ctx.validateBody('name') - .required('the "name" parameter is required') - .notEmpty() - .isString('the "name" parameter should be String type') - .val() - const description = ctx.validateBody('description') - .optional() - .isString('the "description" parameter should be String type') - .val() - const ext = ctx.validateBody('extends') - .optional() - .isArray('the "extends" parameter should be Array type') - .val() - - const { length } = await TagModel.find({ name }).exec().catch(err => { + const name = ctx.validateBody('name').required('缺少标签名称').notEmpty().val() + const description = ctx.validateBody('description').optional().val() + const ext = ctx.validateBody('extends').optional().toArray().val() + + const { length } = await tagProxy.find({ name }).exec().catch(err => { ctx.log.error(err.message) return [] }) if (!length) { - const data = await new TagModel({ + const data = await tagProxy.newAndSave({ name, description, extends: ext - }).save().catch(err => { - ctx.log.error(err.message) - return null }) - if (data) { - return ctx.success(data) - } else { - ctx.fail() - } + data && data.length + ? ctx.success(data, '标签创建成功') + : ctx.fail('标签创建失败') } else { - ctx.fail(-1, `the tag(${name}) is already exist`) + ctx.fail(`【${name}】标签已经存在`) } } +// 标签更新 exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - const name = ctx.validateBody('name') - .optional() - .isString('the "name" parameter should be String type') - .val() - const description = ctx.validateBody('description') - .optional() - .isString('the "description" parameter should be String type') - .val() - + const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() + const name = ctx.validateBody('name').optional().val() + const description = ctx.validateBody('description').optional().val() const tag = {} name && (tag.name = name) description && (tag.description = description) - const data = await TagModel.findByIdAndUpdate(id, tag, { - new: true - }).catch(err => { - ctx.log.error(err.message) - return null - }) + const data = await tagProxy.updateById(id, tag).exec() - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + data + ? ctx.success(data, '标签更新成功') + : ctx.fail('标签更新失败') } +// 标签删除 exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - const data = await TagModel.remove({ _id: id }).catch(err => { - ctx.log.error(err.message) - return null - }) + const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() + const articles = await tagProxy.find({ tag: id }).exec() - if (data && data.result && data.result.ok) { - // 删除所有文章关联关系 - const articles = await ArticleModel.find({ tag: data._id }) - .exec() - .catch(err => { - ctx.log.error(err.message) - return [] - }) - // TODO: 这里应该需要一个容错机制,保证如果有一篇文章没有删除成功的话,需要在规定次数内反复删除 - await Promise.all( - articles.map(item => { - return ArticleModel.findByIdAndUpdate(item._id, { - tag: item.tag.filter(tag => tag.toString() !== data._id.toString()) - }).exec() - }) - ).catch(err => { - ctx.log.error(err.message) - }) - ctx.success() + if (articles && articles.length) { + // 标签下面有文章,不能删除 + ctx.fail('该标签下有文章,不能删除') } else { - ctx.fail() + const data = await tagProxy.delById(id).exec() + data && data.result && data.result.ok + ? ctx.success(null, '标签删除成功') + : ctx.fail('标签删除失败') } } diff --git a/server/controller/user.js b/server/controller/user.js index 9c7c07b..624680d 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -7,11 +7,13 @@ 'use strict' const config = require('../config') +const { userProxy, commentProxy } = require('../proxy') const { UserModel, CommentModel } = require('../model') const { bhash, bcompare, getDebug, proxy } = require('../util') const { getGithubUsersInfo } = require('../service') const debug = getDebug('User') +// 用户列表 exports.list = async (ctx, next) => { let select = '-password' @@ -19,133 +21,111 @@ exports.list = async (ctx, next) => { select += ' -createdAt -updatedAt -role' } - const data = await UserModel.find({}) + const data = await userProxy.find() .sort('-createdAt') .select(select) - .catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + + data + ? ctx.success(data, '用户列表获取成功') + : ctx.fail('用户列表获取失败') } +// 用户详情 exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() + const id = ctx.validateParam('id').required('缺少用户ID').toString().isObjectId().val() let select = '-password' if (!ctx._isAuthenticated) { select += ' -createdAt -updatedAt -github' } - - const data = await UserModel.findById(id) + + const data = await userProxy.getById(id) .select(select) .exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + + data + ? ctx.success(data, '用户详情获取成功') + : ctx.fail('用户详情获取失败') } -exports.update = async (ctx, next) => { - const name = ctx.validateBody('name').optional().isString('the "name" parameter should be String type').val() - const email = ctx.validateBody('email').optional().isString('the "email" parameter should be String type').isEmail('Invalid email format').val() - const site = ctx.validateBody('site').optional().isString('the "site" parameter should be String type').val() - const description = ctx.validateBody('description').optional().isString('the "description" parameter should be String type').val() - const avatar = ctx.validateBody('avatar').optional().isString('the "avatar" parameter should be String type').val() - const slogan = ctx.validateBody('slogan').optional().isString('the "slogan" parameter should be String type').val() - const role = ctx.validateBody('role').optional().toInt().isIn(Object.values(config.constant.roleMap), 'the "role" parameter is not the expected value').val() - const password = ctx.validateBody('password').optional().isString('the "password" parameter should be String type').val() - const mute = ctx.validateBody('mute').optional().toBoolean().val() +// 用户更新,只能更新自己 +exports.updateMe = async (ctx, next) => { + const name = ctx.validateBody('name').optional().val() + const email = ctx.validateBody('email').optional().isEmail('Email格式错误').val() + const site = ctx.validateBody('site').optional().val() + const description = ctx.validateBody('description').optional().val() + const avatar = ctx.validateBody('avatar').optional().val() + const slogan = ctx.validateBody('slogan').optional().val() + const company = ctx.validateBody('company').optional().val() + const location = ctx.validateBody('location').optional().val() const user = {} name && (user.name = name) slogan && (user.slogan = slogan) + company && (user.company = company) + location && (user.location = location) site && (user.site = site) description && (user.description = description) avatar && (user.avatar = avatar) email && (user.email = email) - if (role !== undefined) { - user.role = role - } - - if (mute !== undefined) { - user.mute = mute - } - - if (password !== undefined) { - const oldPassword = ctx.validateBody('old_password') - .required('the "old_password" parameter is required') - .notEmpty() - .isString('the "old_password" parameter should be String type') - .val() + const data = await userProxy.updateById(ctx._user._id, user).exec() + data + ? ctx.success(data, '用户更新成功') + : ctx.fail('用户更新失败') +} - const vertifyPassword = bcompare(oldPassword, ctx._user.password) - if (!vertifyPassword) { - return ctx.fail(-1, 'old password is not correct') - } - user.password = bhash(password) +// 更新密码 +exports.password = async (ctx, next) => { + const password = ctx.validateBody('password').required('缺少新密码').notEmpty().val() + const oldPassword = ctx.validateBody('old_password').required('缺少原密码').notEmpty().val() + const vertifyPassword = bcompare(oldPassword, ctx._user.password) + if (!vertifyPassword) { + return ctx.fail('原密码错误') } - const data = await UserModel.findByIdAndUpdate(ctx._user._id, user, { - new: true - }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + const data = await userProxy.updateById(ctx._user._id, { + password: bhash(password) + }).exec() + data + ? ctx.success(data, '密码更新成功') + : ctx.fail('密码更新失败') } -exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('the "id" parameter is required').toString().isObjectId().val() - const data = await UserModel.remove({ _id: id }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data && data.result && data.result.ok) { - ctx.success() - } else { - ctx.fail() - } +// 用户禁言/解禁 +exports.mute = async (ctx, next) => { + const id = ctx.validateParam('id').required('缺少用户ID').toString().isObjectId().val() + const mute = ctx.validateBody('mute').defaultTo(true).toBoolean().val() + const data = await userProxy.updateById(id, { mute }).exec() + const msg = mute ? '用户禁言' : '用户解禁' + data + ? ctx.success(null, `${msg}成功`) + : ctx.fail(`${msg}失败`) } +// 博主信息获取 exports.blogger = async (ctx, next) => { - const data = await UserModel + const data = await userProxy .findOne({ 'github.login': config.author, role: 0 }) .select('-password -role -createdAt -updatedAt -github -mute') .exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + + data + ? ctx.success(data, '博主详情获取成功') + : ctx.fail('博主详情获取失败') } +// 站内留言墙的用户,只限站内留言 exports.guests = async (ctx, next) => { - // $lookup尝试失败,只能循环查询用户了 - let data = await CommentModel.aggregate([ + // OPTIMIZE: $lookup尝试失败,只能循环查询用户了 + let data = await commentProxy.aggregate([ + { + $match: { + spam: false, // 非垃圾留言 + state: 1, // 审核通过 + type: 1 // 站内留言 + } + }, { $sort: { createdAt: -1 @@ -156,9 +136,10 @@ exports.guests = async (ctx, next) => { _id: '$author' } } - ]) + ]).exec() + let list = await Promise.all((data || []).map((item) => { - return UserModel.findOne({ + return userProxy.findOne({ _id: item._id, $nor: [ { @@ -167,55 +148,11 @@ exports.guests = async (ctx, next) => { 'github.login': config.author } ] - }).select('name site avatar') + }).select('name site avatar').exec() })) list = list.filter(item => !!item) ctx.success({ list, total: list.length - }) -} - -// 更新用户的Github信息 -exports.updateGithubInfo = async () => { - const users = await UserModel.find({}) - .exec() - .catch(err => { - debug.error('用户查找失败,错误:', err.message) - return [] - }) - const githubUsers = users.reduce((sum, user) => { - if (user.role === config.constant.roleMap.GITHUB_USER || (user.role === config.constant.roleMap.ADMIN && user.github.login)) { - sum.push(user) - } - return sum - }, []) - const updates = await getGithubUsersInfo(githubUsers.map(user => user.github.login)) - Promise.all( - updates.reduce((tasks, data, index) => { - const user = githubUsers[index] - const u = { - name: data.name, - email: data.email, - avatar: proxy(data.avatar_url), - site: data.blog || data.url, - slogan: data.bio, - company: data.company, - location: data.location, - github: { - id: data.id, - login: data.login - } - } - tasks.push( - UserModel.findByIdAndUpdate(user._id, u).exec().catch(err => { - debug.error('Github用户信息更新失败,错误:', err.message) - return null - }) - ) - return tasks - }, []) - ).then(() => { - debug.success('所有Github用户信息更新成功') - }) + }, '站内留言用户列表获取成功') } diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index 54e31bd..a108a95 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -10,7 +10,7 @@ const compose = require('koa-compose') const jwt = require('jsonwebtoken') const passport = require('koa-passport') const config = require('../config') -const { UserModel } = require('../model') +const { userProxy } = require('../proxy') const debug = require('../util').getDebug('Auth') const isProd = process.env.NODE_ENV === 'production' const redirectReg = /auth\/github\/login(.*?)/ @@ -75,8 +75,8 @@ exports.isAuthenticated = () => { } const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) - const user = await UserModel.findById(userId).exec().catch(err => { - debug.error('用户查找失败, 错误:', err.message) + const user = await userProxy.getById(userId).exec().catch(err => { + debug.error('token验证时用户查找失败, 错误:', err.message) ctx.log.error(err.message) return null }) diff --git a/server/model/schema/user.js b/server/model/schema/user.js index 6a14bf2..fdb119a 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -17,7 +17,7 @@ const userSchema = new mongoose.Schema({ site: { type: String, validate: isSiteUrl }, slogan: { type: String }, description: { type: String, default: '' }, - // 角色 0 管理员 | 1 普通用户 | 2 github用户 + // 角色 0 管理员 | 1 普通用户 | 2 github用户,不能更改 role: { type: Number, default: 1 }, // role = 0的时候才有该项 password: { type: String }, @@ -27,6 +27,7 @@ const userSchema = new mongoose.Schema({ location: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, + // github信息,不能手动更改 github: { id: { type: String, default: '' }, login: { type: String, default: '' } diff --git a/server/plugins/crontab.js b/server/plugins/crontab.js new file mode 100644 index 0000000..106297a --- /dev/null +++ b/server/plugins/crontab.js @@ -0,0 +1,21 @@ +/** + * @desc 定时任务 + * @author Jooger + * @date 27 Oct 2017 + */ + +'use strict' + +const { getDebug } = require('../util') +const { modelUpdate } = require('../service') +const debug = getDebug('Crontab') + +exports.start = () => { + // 友链 每1小时更新一次 + modelUpdate.updateOption() + setInterval(modelUpdate.updateOption, 1000 * 60 * 60 * 1) + + // 用户 每1天更新一次 + modelUpdate.updateGithubInfo() + setInterval(modelUpdate.updateGithubInfo, 1000 * 60 * 60 * 24) +} diff --git a/server/plugins/gc.js b/server/plugins/gc.js index b8b23b3..684c360 100644 --- a/server/plugins/gc.js +++ b/server/plugins/gc.js @@ -9,15 +9,18 @@ const gc = require('idle-gc') const gcStat = require('gc-stats') const debug = require('../util').getDebug('GC') +const isProd = process.env.NODE_ENV === 'production' exports.start = (interval = 5000, delay = 5000) => { - setTimeout(() => { - gcStat().on('stats', stats => { - debug('回收完毕,用时 %s ms,共回收 %s KB堆内存', stats.pauseMS, stats.diff.totalHeapSize / 1000) - }) - gc.start(interval) - debug.success('服务启动成功') - }, delay) + if (isProd) { + setTimeout(() => { + gcStat().on('stats', stats => { + debug('回收完毕,用时 %s ms,共回收 %s KB堆内存', stats.pauseMS, stats.diff.totalHeapSize / 1000) + }) + gc.start(interval) + debug.success('服务启动成功') + }, delay) + } } exports.stop = () => gc.stop() diff --git a/server/plugins/index.js b/server/plugins/index.js index cea5b60..a6b5ed7 100644 --- a/server/plugins/index.js +++ b/server/plugins/index.js @@ -12,3 +12,4 @@ exports.akismet = require('./akismet') exports.validation = require('./validation') exports.mailer = require('./mailer') exports.gc = require('./gc') +exports.crontab = require('./crontab') diff --git a/server/proxy/article.js b/server/proxy/article.js index 6eafc00..c9c0818 100644 --- a/server/proxy/article.js +++ b/server/proxy/article.js @@ -6,66 +6,13 @@ 'use strict' +const BaseProxy = require('./base') const { ArticleModel } = require('../model') -module.exports = class ArticleProxy { - static newAndSave (docs) { - if (!Array.isArray(docs)) { - docs = [docs] - } - return ArticleModel.insertMany(docs) - } - - static paginate (query, opt = {}) { - return ArticleModel.paginate(query, opt) - } - - static getById (articleId) { - return ArticleModel.findById(articleId) - } - - static find (query = {}, opt = {}) { - return ArticleModel.find(query, null, opts) - } - - static findOne (query = {}, opt = {}) { - return ArticleModel.findOne(query, null, opt) - } - - static updateById (id, doc, opt = {}) { - return ArticleModel.findByIdAndUpdate(id, doc, { - new: true, - ...opt - }) - } - - static updateOne (query = {}, doc = {}, opt = {}) { - return ArticleModel.findOneAndUpdate(query, doc, { - new: true, - ...opt - }) - } - - static updateMany (query = {}, doc = {}, opt = {}) { - return ArticleModel.update(query, doc, { - multi: true, - ...opt - }) - } - - static del (query) { - return ArticleModel.remove(query) - } - - static delById (articleId) { - return this.del({ _id: articleId }) - } - - static delByIds (articleIds) { - return this.del({ - _id: { - $in: articleIds - } - }) +class ArticleProxy extends BaseProxy { + constructor () { + super(ArticleModel) } } + +module.exports = new ArticleProxy() diff --git a/server/proxy/base.js b/server/proxy/base.js new file mode 100644 index 0000000..ee37b15 --- /dev/null +++ b/server/proxy/base.js @@ -0,0 +1,77 @@ +/** + * @desc Base model proxy + * @author Jooger + * @date 27 Jan 2018 + */ + +'use strict' + +module.exports = class BaseProxy { + constructor (Model) { + this.Model = Model + } + + newAndSave (docs) { + if (!Array.isArray(docs)) { + docs = [docs] + } + return this.Model.insertMany(docs) + } + + paginate (query, opt = {}) { + return this.Model.paginate(query, opt) + } + + getById (id) { + return this.Model.findById(id) + } + + find (query = {}, opt = {}) { + return this.Model.find(query, null, opt) + } + + findOne (query = {}, opt = {}) { + return this.Model.findOne(query, null, opt) + } + + updateById (id, doc, opt = {}) { + return this.Model.findByIdAndUpdate(id, doc, { + new: true, + ...opt + }) + } + + updateOne (query = {}, doc = {}, opt = {}) { + return this.Model.findOneAndUpdate(query, doc, { + new: true, + ...opt + }) + } + + updateMany (query = {}, doc = {}, opt = {}) { + return this.Model.update(query, doc, { + multi: true, + ...opt + }) + } + + del (query) { + return this.Model.remove(query) + } + + delById (id) { + return this.del({ _id: id }) + } + + delByIds (ids) { + return this.del({ + _id: { + $in: ids + } + }) + } + + aggregate (opt = {}) { + return this.Model.aggregate(opt) + } +} diff --git a/server/proxy/category.js b/server/proxy/category.js new file mode 100644 index 0000000..f5b60d3 --- /dev/null +++ b/server/proxy/category.js @@ -0,0 +1,18 @@ +/** + * @desc Category model proxy + * @author Jooger + * @date 27 Jan 2018 + */ + +'use strict' + +const BaseProxy = require('./base') +const { CategoryModel } = require('../model') + +class CategoryProxy extends BaseProxy { + constructor () { + super(CategoryModel) + } +} + +module.exports = new CategoryProxy() diff --git a/server/proxy/comment.js b/server/proxy/comment.js new file mode 100644 index 0000000..019a1d4 --- /dev/null +++ b/server/proxy/comment.js @@ -0,0 +1,18 @@ +/** + * @desc Comment model proxy + * @author Jooger + * @date 27 Jan 2018 + */ + +'use strict' + +const BaseProxy = require('./base') +const { CommentModel } = require('../model') + +class CommentProxy extends BaseProxy { + constructor () { + super(CommentModel) + } +} + +module.exports = new CommentProxy() diff --git a/server/proxy/index.js b/server/proxy/index.js index 8a02546..acdd63e 100644 --- a/server/proxy/index.js +++ b/server/proxy/index.js @@ -7,5 +7,11 @@ 'use strict' module.exports = { - ArticleProxy: require('./article') + articleProxy: require('./article'), + categoryProxy: require('./category'), + tagProxy: require('./tag'), + userProxy: require('./user'), + commentProxy: require('./comment'), + optionProxy: require('./option'), + momentProxy: require('./moment') } diff --git a/server/proxy/moment.js b/server/proxy/moment.js new file mode 100644 index 0000000..f14dfa7 --- /dev/null +++ b/server/proxy/moment.js @@ -0,0 +1,18 @@ +/** + * @desc Moment model proxy + * @author Jooger + * @date 28 Jan 2018 + */ + +'use strict' + +const BaseProxy = require('./base') +const { MomentModel } = require('../model') + +class MomentProxy extends BaseProxy { + constructor () { + super(MomentModel) + } +} + +module.exports = new MomentProxy() diff --git a/server/proxy/option.js b/server/proxy/option.js new file mode 100644 index 0000000..7a37837 --- /dev/null +++ b/server/proxy/option.js @@ -0,0 +1,18 @@ +/** + * @desc Option model proxy + * @author Jooger + * @date 28 Jan 2018 + */ + +'use strict' + +const BaseProxy = require('./base') +const { OptionModel } = require('../model') + +class OptionProxy extends BaseProxy { + constructor () { + super(OptionModel) + } +} + +module.exports = new OptionProxy() diff --git a/server/proxy/tag.js b/server/proxy/tag.js new file mode 100644 index 0000000..690e331 --- /dev/null +++ b/server/proxy/tag.js @@ -0,0 +1,18 @@ +/** + * @desc Tag model proxy + * @author Jooger + * @date 27 Jan 2018 + */ + +'use strict' + +const BaseProxy = require('./base') +const { TagModel } = require('../model') + +class TagProxy extends BaseProxy { + constructor () { + super(TagModel) + } +} + +module.exports = new TagProxy() diff --git a/server/proxy/user.js b/server/proxy/user.js new file mode 100644 index 0000000..8189ad5 --- /dev/null +++ b/server/proxy/user.js @@ -0,0 +1,18 @@ +/** + * @desc User model proxy + * @author Jooger + * @date 27 Jan 2018 + */ + +'use strict' + +const BaseProxy = require('./base') +const { UserModel } = require('../model') + +class UserProxy extends BaseProxy { + constructor () { + super(UserModel) + } +} + +module.exports = new UserProxy() diff --git a/server/routes/backend.js b/server/routes/backend.js index 5342344..896cc0b 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -58,9 +58,12 @@ router.patch('/options', isAuthenticated, option.update) // User router.get('/users', isAuthenticated, user.list) +router.get('/users/blogger', isAuthenticated, user.blogger) +router.get('/users/guests', isAuthenticated, user.guests) router.get('/users/:id', isAuthenticated, user.item) -router.patch('/users/:id', isAuthenticated, user.update) -router.delete('/users/:id', isAuthenticated, user.delete) +router.patch('/users/me/password', isAuthenticated, user.password) +router.patch('/users/me', isAuthenticated, user.updateMe) +router.patch('/users/:id/mute', isAuthenticated, user.mute) // Music router.get('/music/songs', isAuthenticated, music.list) diff --git a/server/routes/frontend.js b/server/routes/frontend.js index 435c8ab..2471ddf 100644 --- a/server/routes/frontend.js +++ b/server/routes/frontend.js @@ -22,7 +22,7 @@ const { // Article router.get('/articles', article.list) router.get('/articles/hot', article.hot) -router.get('/articles/archives', article.archive) +router.get('/articles/archives', article.archives) router.get('/articles/:id', article.item) router.post('/articles/:id/like', article.like) diff --git a/server/service/crontab.js b/server/service/crontab.js deleted file mode 100644 index 73b0ca0..0000000 --- a/server/service/crontab.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @desc 定时任务 - * @author Jooger - * @date 27 Oct 2017 - */ - -'use strict' - -exports.start = () => { - const { option, user } = require('../controller') - // 友链 每1小时更新一次 - option.updateOptionLinks() - setInterval(() => option.updateOptionLinks(), 1000 * 60 * 60 * 1) - - // 用户 每1天更新一次 - user.updateGithubInfo() - setInterval(() => user.updateGithubInfo(), 1000 * 60 * 60 * 24) -} diff --git a/server/service/github-userinfo.js b/server/service/github-userinfo.js index d17aac2..514a089 100644 --- a/server/service/github-userinfo.js +++ b/server/service/github-userinfo.js @@ -33,13 +33,13 @@ exports.getGithubUsersInfo = (githubNames = '') => { } }).then(res => { if (res && res.status === 200) { - debug.success('抓取【 %s 】信息成功', name) + debug.success('【 %s 】信息抓取成功', name) return res.data } return null }) .catch(err => { - debug.error('抓取【 %s 】信息失败,错误:%s', name, err.message) + debug.error('【 %s 】信息抓取失败,错误:%s', name, err.message) return null }) }) diff --git a/server/service/index.js b/server/service/index.js index c26a0de..ca5cd85 100644 --- a/server/service/index.js +++ b/server/service/index.js @@ -12,5 +12,5 @@ const { getGithubUsersInfo, getGithubAuthUserInfo } = require('./github-userinfo exports.getGithubUsersInfo = getGithubUsersInfo exports.getGithubAuthUserInfo = getGithubAuthUserInfo exports.getGithubToken = require('./github-token') -exports.fetchNE = require('./netease-music') -exports.crontab = require('./crontab') +exports.netease = require('./netease-music') +exports.modelUpdate = require('./model-update') diff --git a/server/service/model-update.js b/server/service/model-update.js new file mode 100644 index 0000000..877de7c --- /dev/null +++ b/server/service/model-update.js @@ -0,0 +1,189 @@ +/** + * @desc Models update + * @author Jooger + * @date 28 Jan 2018 + */ + +'use strict' + +const config = require('../config') +const { userProxy, optionProxy } = require('../proxy') +const { getGithubUsersInfo } = require('./github-userinfo') +const netease = require('./netease-music') +const { getDebug, proxy } = require('../util') +const debug = getDebug('ModelUpdate') +const isProd = process.env.NODE_ENV === 'production' + +// update lock +let updateOptionLock = false +exports.updateOption = async (option = null) => { + if (updateOptionLock) { + debug.warn('站点参数更新中...') + return + } + updateOptionLock = true + if (!option) { + option = await optionProxy.findOne().exec().catch(err => { + debug.error('数据查找失败,错误:', err.message) + ctx.log.error(err.message) + return {} + }) + } + + // 更新友链 + option.links = await generateLinks(option.links) + + const data = await optionProxy.updateOne({}, option).exec().catch(err => { + debug.error('数据更新失败,错误:', err.message) + ctx.log.error(err.message) + return null + }) + + if (data) { + debug.success('站点参数更新成功') + } + updateOptionLock = false + return data +} + +// 更新github用户信息 +exports.updateGithubInfo = async () => { + const users = await userProxy.find() + .exec() + .catch(err => { + debug.error('用户查找失败,错误:', err.message) + return [] + }) + const githubUsers = users.reduce((sum, user) => { + if (user.role === config.constant.roleMap.GITHUB_USER || (user.role === config.constant.roleMap.ADMIN && user.github.login)) { + sum.push(user) + } + return sum + }, []) + const updates = await getGithubUsersInfo(githubUsers.map(user => user.github.login)) + Promise.all( + updates.reduce((tasks, data, index) => { + const user = githubUsers[index] + const u = { + name: data.name, + email: data.email, + avatar: proxy(data.avatar_url), + site: data.blog || data.url, + slogan: data.bio, + company: data.company, + location: data.location, + github: { + id: data.id, + login: data.login + } + } + tasks.push( + userProxy.updateById(user._id, u).exec().catch(err => { + debug.error('Github用户信息更新失败,错误:', err.message) + return null + }) + ) + return tasks + }, []) + ).then(() => { + debug.success('所有Github用户信息更新成功') + }) +} + +// 获取除了歌曲链接和歌词外其他信息 +exports.fetchSonglist = (playListId) => { + return netease.neteaseMusic._playlist(playListId).then(({ playlist }) => { + if (!playlist) { + return null + } + const tracks = playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { + return { + id, + name, + duration: dt || 0, + album: al && { + name: al.name, + cover: isProd ? (proxy(al.picUrl) || '') : al.picUrl, + tns: al.tns + } || {}, + artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], + tns: tns || [] + } + }) + return { + id: playListId, + tracks, + name: playlist.name, + description: playlist.description, + tags: playlist.tags + } + }).catch(err => { + debug.error('歌单列表获取失败,错误:', err.message) + return null + }) +} + +// 更新song list cache +let musicCacheLock = false +exports.updateMusicCache = async function (playListId = '') { + const { redis } = require('../plugins') + if (musicCacheLock) { + debug.warn('缓存更新中...') + return redis.get(config.constant.redisCacheKey.music) || null + } + musicCacheLock = true + if (!playListId) { + const option = await optionProxy.findOne().exec().catch(err => { + debug.error('Option查找失败,错误:', err.message) + return null + }) + + if (!option || !option.musicId) { + debug.warn('歌单ID未配置') + musicCacheLock = false + return redis.get(config.constant.redisCacheKey.music) || null + } + playListId = option.musicId + } + + const data = await exports.fetchSonglist(playListId) + if (!data) { + musicCacheLock = false + return redis.get(config.constant.redisCacheKey.music) || null + } + const set = { + id: playListId, + data + } + + // 设置10分钟过期 + redis.set(config.constant.redisCacheKey.music, set, 60 * 10).then(() => { + debug.success('缓存更新成功,歌单ID:', playListId) + }).catch(err => { + debug.error('缓存更新失败,歌单ID:%s,错误:%s', playListId, err.message) + }) + + musicCacheLock = false + return set +} + +// 更新友链 +async function generateLinks (links = []) { + if (links && links.length) { + const githubNames = links.map(link => link.github) + const usersInfo = await getGithubUsersInfo(githubNames) + + if (usersInfo) { + return links.map((link, index) => { + const userInfo = usersInfo[index] + if (userInfo) { + link.avatar = proxy(userInfo.avatar_url) + link.slogan = userInfo.bio + link.site = link.site || userInfo.blog || userInfo.url + } + return link + }) + } + } + return links +} diff --git a/server/service/netease-music.js b/server/service/netease-music.js index 8fed020..ff09db3 100644 --- a/server/service/netease-music.js +++ b/server/service/netease-music.js @@ -7,7 +7,9 @@ 'use strict' const axios = require('axios') +const NeteseMusic = require('simple-netease-cloud-music') const { encrypt, getDebug } = require('../util') +const neteaseMusic = new NeteseMusic() const debug = getDebug('Netease') const neFetcher = axios.create({ @@ -93,4 +95,7 @@ const fetchNE = function (type = '', id = '') { }) } -module.exports = fetchNE +module.exports = { + neteaseMusic, + fetchNE +} From 7d414d9af61135dde6867776b3290526e3cc5f33 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Thu, 1 Feb 2018 11:45:32 +0800 Subject: [PATCH 092/208] [fix] fix netease ref error --- server/controller/music.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/controller/music.js b/server/controller/music.js index 83a9706..99e3d35 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -91,7 +91,7 @@ exports.lyric = async (ctx, next) => { .isString('the "song_id" parameter should be String type') .val() - const data = await neteaseMusic.lyric(songId) + const data = await netease.neteaseMusic.lyric(songId) ctx.success(data) } @@ -103,7 +103,7 @@ exports.cover = async (ctx, next) => { .isString('the "cover_id" parameter should be String type') .val() - const data = await neteaseMusic.picture(coverId) + const data = await netease.neteaseMusic.picture(coverId) ctx.success(data) } From 6094cabecc33a798ef1418be4b65f917d3e0ec5d Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Thu, 1 Feb 2018 13:30:46 +0800 Subject: [PATCH 093/208] [update] add eslint supports --- .eslintignore | 2 + .eslintrc.js | 24 + package.json | 17 +- server/app.js | 9 +- server/config/development.js | 22 +- server/config/index.js | 168 ++-- server/config/production.js | 26 +- server/config/test.js | 2 + server/controller/article.js | 755 +++++++++--------- server/controller/auth.js | 271 ++++--- server/controller/category.js | 204 ++--- server/controller/comment.js | 1200 ++++++++++++++--------------- server/controller/moment.js | 210 +++-- server/controller/music.js | 137 ++-- server/controller/option.js | 21 +- server/controller/tag.js | 182 ++--- server/controller/user.js | 231 +++--- server/middleware/authenticate.js | 168 ++-- server/middleware/error.js | 45 +- server/middleware/formidable.js | 44 +- server/middleware/header.js | 38 +- server/middleware/response.js | 48 +- server/model/index.js | 28 +- server/model/schema/article.js | 66 +- server/model/schema/category.js | 20 +- server/model/schema/comment.js | 46 +- server/model/schema/log.js | 2 +- server/model/schema/moment.js | 12 +- server/model/schema/option.js | 66 +- server/model/schema/tag.js | 16 +- server/model/schema/user.js | 42 +- server/plugins/akismet.js | 214 ++--- server/plugins/crontab.js | 14 +- server/plugins/gc.js | 18 +- server/plugins/mailer.js | 73 +- server/plugins/mongo.js | 111 ++- server/plugins/redis.js | 85 +- server/plugins/validation.js | 46 +- server/proxy/article.js | 6 +- server/proxy/base.js | 110 +-- server/proxy/category.js | 6 +- server/proxy/comment.js | 6 +- server/proxy/index.js | 14 +- server/proxy/moment.js | 6 +- server/proxy/option.js | 6 +- server/proxy/tag.js | 6 +- server/proxy/user.js | 6 +- server/routes/backend.js | 22 +- server/routes/index.js | 38 +- server/service/github-passport.js | 125 ++- server/service/github-token.js | 37 +- server/service/github-userinfo.js | 77 +- server/service/model-update.js | 290 ++++--- server/service/netease-music.js | 147 ++-- server/util/debug.js | 65 +- server/util/encrypt.js | 66 +- server/util/gravatar.js | 26 +- server/util/index.js | 62 +- server/util/location.js | 14 +- server/util/marked.js | 132 ++-- server/util/sign-token.js | 4 +- 61 files changed, 2968 insertions(+), 2986 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..926675b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules/* +test/* diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..8193f62 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + root: true, + env: { + es6: true, + node: true + }, + extends: 'koa', + rules: { + "no-tabs": 0, + "indent": ["error", 'tab'], + 'arrow-parens': [2, 'as-needed'], + eqeqeq: 0, + 'no-return-assign': 0, // fails for arrow functions + 'no-var': 2, + semi: [0, 'always'], + 'space-before-function-paren': [2, 'always'], + yoda: 0, + 'arrow-spacing': 2, + 'dot-location': [2, 'property'], + 'prefer-arrow-callback': 2, + "prefer-promise-reject-errors": 0 + }, + globals: {} +} diff --git a/package.json b/package.json index cc732d4..f5b2519 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "pm2": "pm2 startOrReload ecosystem.config.js", "pm2:prod": "pm2 startOrReload ecosystem.config.js --env production", "deploy": "pm2 deploy ecosystem.config.js production", - "test": "echo \"Error: no test specified\" && exit 1" + "precommit": "npm run lint", + "lint": "eslint --ext .js,.ts --ignore-path .gitignore ." }, "dependencies": { "akismet-api": "^3.0.0", @@ -73,6 +74,20 @@ }, "devDependencies": { "cross-env": "^5.0.5", + "eslint": "^4.16.0", + "eslint-config-koa": "^2.0.2", + "eslint-config-standard": "^11.0.0-beta.0", + "eslint-plugin-import": "^2.8.0", + "eslint-plugin-node": "^5.2.1", + "eslint-plugin-promise": "^3.6.0", + "eslint-plugin-standard": "^3.0.1", "nodemon": "^1.8.1" + }, + "config": { + "pre-git": { + "pre-commit": [ + "npm run precommit" + ] + } } } diff --git a/server/app.js b/server/app.js index 099279e..6901bc1 100644 --- a/server/app.js +++ b/server/app.js @@ -31,19 +31,18 @@ bouncer.Validator = validation // error handler onerror(app) - // middlewares app.use(bodyparser({ - enableTypes:['json', 'form', 'text'] + enableTypes: ['json', 'form', 'text'] })) app.use(json()) app.use(logger()) app.use(koaBunyanLogger({ - name: packageInfo.name, - level: 'debug' + name: packageInfo.name, + level: 'debug' })) app.use(koaBunyanLogger.requestIdContext({ - header: 'Request-Id' + header: 'Request-Id' })) app.use(bouncer.middleware()) app.use(middlewares.response) diff --git a/server/config/development.js b/server/config/development.js index dbff1c6..f586da8 100644 --- a/server/config/development.js +++ b/server/config/development.js @@ -7,15 +7,15 @@ 'use strict' module.exports = { - mongo: { - uri: 'mongodb://127.0.0.1/jooger-me-dev' - }, - sns: { - github: { - // 测试用的ID和Secret - clientID: '5b4d4a7945347d0fd2e2', - clientSecret: '8771bd9ae52749cc15b0c9e2c6cb4ecd7f39d9da', - callbackURL: 'http://127.0.0.1:3001/auth/github/login/callback' - } - } + mongo: { + uri: 'mongodb://127.0.0.1/jooger-me-dev' + }, + sns: { + github: { + // 测试用的ID和Secret + clientID: '5b4d4a7945347d0fd2e2', + clientSecret: '8771bd9ae52749cc15b0c9e2c6cb4ecd7f39d9da', + callbackURL: 'http://127.0.0.1:3001/auth/github/login/callback' + } + } } diff --git a/server/config/index.js b/server/config/index.js index 195a4d6..474af62 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -11,90 +11,90 @@ const _ = require('lodash') const packageInfo = require('../../package.json') const baseConfig = { - name: packageInfo.name, - version: packageInfo.version, - author: packageInfo.author.name, - site: packageInfo.author.url, - email: packageInfo.author.email, - env: process.env.NODE_ENV, - root: path.resolve(__dirname, '../../'), - port: process.env.PORT || 3001, - // 限制参数 - limit: { - articleLimit: 3, - // 相关文章限制个数 - relatedArticleLimit: 10, - hotLimit: 7, - commentLimit: 20, - momentLimit: 10, - // 垃圾评论允许的最大发布次数 - commentSpamLimit: 3, - }, - mongo: { - option: { - useMongoClient: true, - poolSize: 20, - keepAlive: true, - autoReconnect: true, - reconnectInterval: 1000, - reconnectTries: Number.MAX_VALUE - } - }, - redis: { - host: '127.0.0.1', - port: 6379 - }, - auth: { - session: { - key: 'jooger.me.token', - maxAge: 60000 * 60 * 24 * 7, - signed: false - }, - userCookieKey: 'jooger.me.userid', - secrets: `${packageInfo.name}-secrets`, - defaultAvatar: 'http://static.jooger.me/img/common/default-avatar.png', - // 初始化管理员,默认github账户名 - defaultName: packageInfo.author.name, - defaultPassword: 'admin_jooger' - }, - sns: { - github: { - // 登陆后的token的cookie名,每个第三方登录方式必备项 - key: 'jooger.me.github.token', - clientID: process.env.githubClientID || 'github client id', - clientSecret: process.env.githubClientSecret || 'github client secret', - callbackURL: 'github oauth callback url' - } - }, - akismet: { - apiKey: process.env.akismetApikey || 'akismet api key' - }, - constant: { - // 允许请求的域名 - allowedOrigins: [ - 'jooger.me', - 'www.jooger.me', - 'admin.jooger.me' - ], - codeMap: { - '-1': '请求失败', - '200': '请求成功', - '401': '权限校验失败', - '403': 'Forbidden', - '500': '服务器错误', - '10001': '参数错误' - }, - // 角色 - roleMap: { - ADMIN: 0, - USER: 1, - GITHUB_USER: 2 - }, - monthMap: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], - redisCacheKey: { - music: 'music-data' - } - } + name: packageInfo.name, + version: packageInfo.version, + author: packageInfo.author.name, + site: packageInfo.author.url, + email: packageInfo.author.email, + env: process.env.NODE_ENV, + root: path.resolve(__dirname, '../../'), + port: process.env.PORT || 3001, + // 限制参数 + limit: { + articleLimit: 3, + // 相关文章限制个数 + relatedArticleLimit: 10, + hotLimit: 7, + commentLimit: 20, + momentLimit: 10, + // 垃圾评论允许的最大发布次数 + commentSpamLimit: 3 + }, + mongo: { + option: { + useMongoClient: true, + poolSize: 20, + keepAlive: true, + autoReconnect: true, + reconnectInterval: 1000, + reconnectTries: Number.MAX_VALUE + } + }, + redis: { + host: '127.0.0.1', + port: 6379 + }, + auth: { + session: { + key: 'jooger.me.token', + maxAge: 60000 * 60 * 24 * 7, + signed: false + }, + userCookieKey: 'jooger.me.userid', + secrets: `${packageInfo.name}-secrets`, + defaultAvatar: 'http://static.jooger.me/img/common/default-avatar.png', + // 初始化管理员,默认github账户名 + defaultName: packageInfo.author.name, + defaultPassword: 'admin_jooger' + }, + sns: { + github: { + // 登陆后的token的cookie名,每个第三方登录方式必备项 + key: 'jooger.me.github.token', + clientID: process.env.githubClientID || 'github client id', + clientSecret: process.env.githubClientSecret || 'github client secret', + callbackURL: 'github oauth callback url' + } + }, + akismet: { + apiKey: process.env.akismetApikey || 'akismet api key' + }, + constant: { + // 允许请求的域名 + allowedOrigins: [ + 'jooger.me', + 'www.jooger.me', + 'admin.jooger.me' + ], + codeMap: { + '-1': '请求失败', + '200': '请求成功', + '401': '权限校验失败', + '403': 'Forbidden', + '500': '服务器错误', + '10001': '参数错误' + }, + // 角色 + roleMap: { + ADMIN: 0, + USER: 1, + GITHUB_USER: 2 + }, + monthMap: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + redisCacheKey: { + music: 'music-data' + } + } } module.exports = _.merge(baseConfig, require(`./${process.env.NODE_ENV}`)) diff --git a/server/config/production.js b/server/config/production.js index 73ac5e3..6d2b781 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -7,17 +7,17 @@ 'use strict' module.exports = { - mongo: { - uri: 'mongodb://127.0.0.1/jooger-me' - }, - auth: { - session: { - domain: '.jooger.me' - } - }, - sns: { - github: { - callbackURL: 'https://api.jooger.me/auth/github/login/callback' - } - } + mongo: { + uri: 'mongodb://127.0.0.1/jooger-me' + }, + auth: { + session: { + domain: '.jooger.me' + } + }, + sns: { + github: { + callbackURL: 'https://api.jooger.me/auth/github/login/callback' + } + } } diff --git a/server/config/test.js b/server/config/test.js index 48404c8..5e15e20 100644 --- a/server/config/test.js +++ b/server/config/test.js @@ -5,3 +5,5 @@ */ 'use strict' + +module.exports = {} diff --git a/server/controller/article.js b/server/controller/article.js index ba590a6..68424fb 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -8,357 +8,356 @@ const config = require('../config') const { articleProxy, categoryProxy, tagProxy } = require('../proxy') -const { marked, isObjectId, createObjectId, getDebug, getMonthFromNum, getDocsPaginationData } = require('../util') -const debug = getDebug('Article') +const { marked, isObjectId, createObjectId, getMonthFromNum, getDocsPaginationData } = require('../util') // 文章列表 exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.articleLimit).toInt().gt(0, 'per_page参数必须大于0').val() - const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'page参数必须大于0').val() - const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'state参数错误').val() - const category = ctx.validateQuery('category').optional().toString().val() - const tag = ctx.validateQuery('tag').optional().toString().val() - const keyword = ctx.validateQuery('keyword').optional().toString().val() - // 时间区间查询仅后台可用,且依赖于createdAt - const startDate = ctx.validateQuery('start_date').optional().toString().val() - const endDate = ctx.validateQuery('end_date').optional().toString().val() - // 排序仅后台能用,且order和sortBy需同时传入才起作用 - // -1 desc | 1 asc - const order = ctx.validateQuery('order').optional().toInt().isIn( - [-1, 1], - 'order参数错误' - ).val() - // createdAt | updatedAt | publishedAt | meta.ups | meta.pvs | meta.comments - const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn( - ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], - 'sort_by参数错误' - ).val() - - // 过滤条件 - const options = { - sort: { - createdAt: -1 - }, - page, - limit: pageSize, - select: '-content -renderedContent', - populate: [ - { - path: 'category', - select: 'name description extends' - }, { - path: 'tag', - select: 'name description' - } - ] - } - - // 查询条件 - const query = {} - - if (state !== undefined) { - query.state = state - } - - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { title: keywordReg } - ] - } - - // 分类 - if (category) { - // 如果是id - if (isObjectId(category)) { - query.category = category - } else { - // 普通字符串,需要先查到id - const c = await categoryProxy.findOne({ name: category }).exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - query.category = c ? c._id : createObjectId() - } - } - - // 标签 - if (tag) { - // 如果是id - if (isObjectId(tag)) { - query.tag = tag - } else { - // 普通字符串,需要先查到id - const t = await tagProxy.findOne({ name: tag }).exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - query.tag = t ? t._id : createObjectId() - } - } - - // 未通过权限校验(前台获取文章列表) - if (!ctx._isAuthenticated) { - // 将文章状态重置为1 - query.state = 1 - // 文章列表不需要content和state - options.select = '-content -renderedContent -state' - } else { - // 排序 - if (sortBy && order) { - options.sort = {} - options.sort[sortBy] = order - } - - // 起始日期 - if (startDate) { - const $gte = new Date(startDate) - if ($gte.toString() !== 'Invalid Date') { - query.createdAt = { $gte } - } - } - - // 结束日期 - if (endDate) { - const $lte = new Date(endDate) - if ($lte.toString() !== 'Invalid Date') { - query.createdAt = Object.assign({}, query.createdAt, { $lte }) - } - } - } - - const articles = await articleProxy.paginate(query, options) - - articles - ? ctx.success(getDocsPaginationData(articles), '文章列表获取成功') - : ctx.fail('文章列表获取失败') + const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.articleLimit).toInt().gt(0, 'per_page参数必须大于0').val() + const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'page参数必须大于0').val() + const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'state参数错误').val() + const category = ctx.validateQuery('category').optional().toString().val() + const tag = ctx.validateQuery('tag').optional().toString().val() + const keyword = ctx.validateQuery('keyword').optional().toString().val() + // 时间区间查询仅后台可用,且依赖于createdAt + const startDate = ctx.validateQuery('start_date').optional().toString().val() + const endDate = ctx.validateQuery('end_date').optional().toString().val() + // 排序仅后台能用,且order和sortBy需同时传入才起作用 + // -1 desc | 1 asc + const order = ctx.validateQuery('order').optional().toInt().isIn( + [-1, 1], + 'order参数错误' + ).val() + // createdAt | updatedAt | publishedAt | meta.ups | meta.pvs | meta.comments + const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn( + ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], + 'sort_by参数错误' + ).val() + + // 过滤条件 + const options = { + sort: { + createdAt: -1 + }, + page, + limit: pageSize, + select: '-content -renderedContent', + populate: [ + { + path: 'category', + select: 'name description extends' + }, { + path: 'tag', + select: 'name description' + } + ] + } + + // 查询条件 + const query = {} + + if (state !== undefined) { + query.state = state + } + + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { title: keywordReg } + ] + } + + // 分类 + if (category) { + // 如果是id + if (isObjectId(category)) { + query.category = category + } else { + // 普通字符串,需要先查到id + const c = await categoryProxy.findOne({ name: category }).exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + query.category = c ? c._id : createObjectId() + } + } + + // 标签 + if (tag) { + // 如果是id + if (isObjectId(tag)) { + query.tag = tag + } else { + // 普通字符串,需要先查到id + const t = await tagProxy.findOne({ name: tag }).exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + query.tag = t ? t._id : createObjectId() + } + } + + // 未通过权限校验(前台获取文章列表) + if (!ctx._isAuthenticated) { + // 将文章状态重置为1 + query.state = 1 + // 文章列表不需要content和state + options.select = '-content -renderedContent -state' + } else { + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } + + // 起始日期 + if (startDate) { + const $gte = new Date(startDate) + if ($gte.toString() !== 'Invalid Date') { + query.createdAt = { $gte } + } + } + + // 结束日期 + if (endDate) { + const $lte = new Date(endDate) + if ($lte.toString() !== 'Invalid Date') { + query.createdAt = Object.assign({}, query.createdAt, { $lte }) + } + } + } + + const articles = await articleProxy.paginate(query, options) + + articles + ? ctx.success(getDocsPaginationData(articles), '文章列表获取成功') + : ctx.fail('文章列表获取失败') } // 热门文章 exports.hot = async (ctx, next) => { - const limit = ctx.validateQuery('limit').defaultTo(config.limit.hotLimit).toInt().gt(0, 'limit参数必须大于0').val() - const data = await articleProxy.find() - .sort('-meta.comments -meta.ups -meta.pvs') - .select('-content -renderedContent -state') - .populate([ - { - path: 'category', - select: 'name' - }, { - path: 'tag', - select: 'name' - } - ]) - .limit(limit) - data - ? ctx.success({ list: data }, '热门文章获取成功') - : ctx.fail('热门文章获取失败') + const limit = ctx.validateQuery('limit').defaultTo(config.limit.hotLimit).toInt().gt(0, 'limit参数必须大于0').val() + const data = await articleProxy.find() + .sort('-meta.comments -meta.ups -meta.pvs') + .select('-content -renderedContent -state') + .populate([ + { + path: 'category', + select: 'name' + }, { + path: 'tag', + select: 'name' + } + ]) + .limit(limit) + data + ? ctx.success({ list: data }, '热门文章获取成功') + : ctx.fail('热门文章获取失败') } // 文章详情 exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() - - let data = null - let query = null - // 只有前台博客访问文章的时候pv才+1 - if (!ctx._isAuthenticated) { - query = articleProxy.updateOne({ _id: id, state: 1 }, { $inc: { 'meta.pvs': 1 } }).select('-content') - } else { - query = articleProxy.getById(id) - } - - data = await query.populate([ - { - path: 'category', - select: 'name description extends' - }, { - path: 'tag', - select: 'name description extends' - } - ]).exec() - - if (data) { - data = data.toObject() - await Promise.all([ - getRelatedArticles(ctx, data), - getSiblingArticles(ctx, data) - ]) - ctx.success(data, '文章详情获取成功') - } else { - ctx.fail('文章详情获取失败') - } + const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() + + let data = null + let query = null + // 只有前台博客访问文章的时候pv才+1 + if (!ctx._isAuthenticated) { + query = articleProxy.updateOne({ _id: id, state: 1 }, { $inc: { 'meta.pvs': 1 } }).select('-content') + } else { + query = articleProxy.getById(id) + } + + data = await query.populate([ + { + path: 'category', + select: 'name description extends' + }, { + path: 'tag', + select: 'name description extends' + } + ]).exec() + + if (data) { + data = data.toObject() + await Promise.all([ + getRelatedArticles(ctx, data), + getSiblingArticles(ctx, data) + ]) + ctx.success(data, '文章详情获取成功') + } else { + ctx.fail('文章详情获取失败') + } } // 文章创建 exports.create = async (ctx, next) => { - const title = ctx.validateBody('title').required('缺少文章标题').notEmpty().val() - const content = ctx.validateBody('content').required('缺少文章内容').notEmpty().val() - const keywords = ctx.validateBody('keywords').optional().toArray().val() - const category = ctx.validateBody('category').optional().isObjectId().val() - const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() - const description = ctx.validateBody('description').optional().val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数错误').val() - const thumb = ctx.validateBody('thumb').optional().val() - const createdAt = ctx.validateBody('createdAt').optional().toString().val() - const permalink = ctx.validateBody('permalink').optional().val() - const article = {} - - title && (article.title = title) - keywords && (article.keywords = keywords) - description && (article.description = description) - category && (article.category = category) - tag && (article.tag = tag) - thumb && (article.thumb = thumb) - createdAt && (article.createdAt = new Date(createdAt)) - permalink && (article.permalink = permalink) - - if (state !== undefined) { - article.state = state - } - article.content = content - article.renderedContent = marked(content) - - let data = await articleProxy.newAndSave(article) - - if (data && data.length) { - data = data[0] - if (!data.permalink) { - // 更新永久链接 - data = await articleProxy.updateById(data._id, { - permalink: `${config.site}/article/${data._id}` - }).exec().catch(err => { - ctx.log.error('文章永久链接更新失败', err.message) - return data - }) - } - ctx.success(data, '文章创建成功') - } else { - ctx.fail('文章创建失败') - } + const title = ctx.validateBody('title').required('缺少文章标题').notEmpty().val() + const content = ctx.validateBody('content').required('缺少文章内容').notEmpty().val() + const keywords = ctx.validateBody('keywords').optional().toArray().val() + const category = ctx.validateBody('category').optional().isObjectId().val() + const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() + const description = ctx.validateBody('description').optional().val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数错误').val() + const thumb = ctx.validateBody('thumb').optional().val() + const createdAt = ctx.validateBody('createdAt').optional().toString().val() + const permalink = ctx.validateBody('permalink').optional().val() + const article = {} + + title && (article.title = title) + keywords && (article.keywords = keywords) + description && (article.description = description) + category && (article.category = category) + tag && (article.tag = tag) + thumb && (article.thumb = thumb) + createdAt && (article.createdAt = new Date(createdAt)) + permalink && (article.permalink = permalink) + + if (state !== undefined) { + article.state = state + } + article.content = content + article.renderedContent = marked(content) + + let data = await articleProxy.newAndSave(article) + + if (data && data.length) { + data = data[0] + if (!data.permalink) { + // 更新永久链接 + data = await articleProxy.updateById(data._id, { + permalink: `${config.site}/article/${data._id}` + }).exec().catch(err => { + ctx.log.error('文章永久链接更新失败', err.message) + return data + }) + } + ctx.success(data, '文章创建成功') + } else { + ctx.fail('文章创建失败') + } } // 文章更新 exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() - const title = ctx.validateBody('title').optional().val() - const content = ctx.validateBody('content').optional().val() - const keywords = ctx.validateBody('keywords').optional().toArray().val() - const description = ctx.validateBody('description').optional().val() - const category = ctx.validateBody('category').optional().isObjectId().val() - const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数错误').val() - const thumb = ctx.validateBody('thumb').optional().val() - const createdAt = ctx.validateBody('createdAt').optional().toString().val() - const article = {} - - title && (article.title = title) - keywords && (article.keywords = keywords) - description && (article.description = description) - category && (article.category = category) - tag && (article.tag = tag) - thumb && (article.thumb = thumb) - createdAt && (article.createdAt = new Date(createdAt)) - - if (state !== undefined) { - article.state = state - } - - if (content !== undefined) { - article.content = content - article.renderedContent = marked(content) - } - - const data = await articleProxy.updateById(id, article).populate('category tag').exec() - - data - ? ctx.success(data, '文章更新成功') - : ctx.fail('文章更新失败') + const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() + const title = ctx.validateBody('title').optional().val() + const content = ctx.validateBody('content').optional().val() + const keywords = ctx.validateBody('keywords').optional().toArray().val() + const description = ctx.validateBody('description').optional().val() + const category = ctx.validateBody('category').optional().isObjectId().val() + const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数错误').val() + const thumb = ctx.validateBody('thumb').optional().val() + const createdAt = ctx.validateBody('createdAt').optional().toString().val() + const article = {} + + title && (article.title = title) + keywords && (article.keywords = keywords) + description && (article.description = description) + category && (article.category = category) + tag && (article.tag = tag) + thumb && (article.thumb = thumb) + createdAt && (article.createdAt = new Date(createdAt)) + + if (state !== undefined) { + article.state = state + } + + if (content !== undefined) { + article.content = content + article.renderedContent = marked(content) + } + + const data = await articleProxy.updateById(id, article).populate('category tag').exec() + + data + ? ctx.success(data, '文章更新成功') + : ctx.fail('文章更新失败') } // 删除文章 exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() - const data = await articleProxy.delById(id).exec() + const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() + const data = await articleProxy.delById(id).exec() - data && data.result && data.result.ok - ? ctx.success(null, '文章删除成功') - : ctx.fail('文章删除失败') + data && data.result && data.result.ok + ? ctx.success(null, '文章删除成功') + : ctx.fail('文章删除失败') } // 文章点赞 exports.like = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() - const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() - const data = await articleProxy.updateById(id, { - $inc: { - 'meta.ups': like ? 1 : -1 - } - }).exec() - - data - ? ctx.success(null, '文章点赞成功') - : ctx.fail('文章点赞失败') + const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() + const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() + const data = await articleProxy.updateById(id, { + $inc: { + 'meta.ups': like ? 1 : -1 + } + }).exec() + + data + ? ctx.success(null, '文章点赞成功') + : ctx.fail('文章点赞失败') } // 文章归档 exports.archives = async (ctx, next) => { - let data = await articleProxy.aggregate([ - { $match: { state: 1 } }, - { $sort: { createdAt: 1 } }, - { - $project: { - year: { $year: '$createdAt' }, - month: { $month: '$createdAt' }, - title: 1, - createdAt: 1 - } - }, - { - $group: { - _id: { - year: '$year', - month: '$month' - }, - articles: { - $push: { - title: '$title', - _id: '$_id', - createdAt: '$createdAt' - } - } - } - } - ]) - - let count = 0 - if (data && data.length) { - data = [...new Set(data.map(item => item._id.year))].map(year => { - const months = [] - data.forEach(item => { - const { _id, articles } = item - if (year === _id.year) { - count += articles.length - months.push({ - month: _id.month, - monthStr: getMonthFromNum(_id.month), - articles - }) - } - }) - return { - year, - months - } - }) - } - ctx.success({ - count, - list: data || [] - }, '获取文章归档成功') + let data = await articleProxy.aggregate([ + { $match: { state: 1 } }, + { $sort: { createdAt: 1 } }, + { + $project: { + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + title: 1, + createdAt: 1 + } + }, + { + $group: { + _id: { + year: '$year', + month: '$month' + }, + articles: { + $push: { + title: '$title', + _id: '$_id', + createdAt: '$createdAt' + } + } + } + } + ]) + + let count = 0 + if (data && data.length) { + data = [...new Set(data.map(item => item._id.year))].map(year => { + const months = [] + data.forEach(item => { + const { _id, articles } = item + if (year === _id.year) { + count += articles.length + months.push({ + month: _id.month, + monthStr: getMonthFromNum(_id.month), + articles + }) + } + }) + return { + year, + months + } + }) + } + ctx.success({ + count, + list: data || [] + }, '获取文章归档成功') } /** @@ -367,28 +366,28 @@ exports.archives = async (ctx, next) => { * @param {} data 文章数据 */ async function getRelatedArticles (ctx, data) { - data.related = [] - let { _id, tag = [] } = data - const articles = await articleProxy.find({ - _id: { $nin: [ _id ] }, - state: 1, - tag: { $in: tag.map(t => t._id) } - }) - .select('title thumb createdAt publishedAt meta category') - .populate({ - path: 'category', - select: 'name description' - }) - .exec() - .catch(err => { - ctx.log.error('关联文章查询失败,err:', err.message) - return null - }) - - if (articles) { - // 最多取前10篇 - data.related = articles.slice(0, config.limit.relatedArticleLimit) - } + data.related = [] + let { _id, tag = [] } = data + const articles = await articleProxy.find({ + _id: { $nin: [ _id ] }, + state: 1, + tag: { $in: tag.map(t => t._id) } + }) + .select('title thumb createdAt publishedAt meta category') + .populate({ + path: 'category', + select: 'name description' + }) + .exec() + .catch(err => { + ctx.log.error('关联文章查询失败,err:', err.message) + return null + }) + + if (articles) { + // 最多取前10篇 + data.related = articles.slice(0, config.limit.relatedArticleLimit) + } } /** @@ -397,41 +396,41 @@ async function getRelatedArticles (ctx, data) { * @param {} data 文章数据 */ async function getSiblingArticles (ctx, data) { - if (data && data._id) { - const query = {} - // 如果未通过权限校验,将文章状态重置为1 - if (!ctx._isAuthenticated) { - query.state = 1 - } - const prev = await articleProxy.findOne(query) - .select('title createdAt publishedAt thumb category') - .populate({ - path: 'category', - select: 'name description' - }) - .sort('-createdAt') - .lt('createdAt', data.createdAt) - .exec() - .catch(err => { - ctx.log.error('前一篇文章获取失败,err:', err.message) - return null - }) - const next = await articleProxy.findOne(query) - .select('title createdAt publishedAt thumb category') - .populate({ - path: 'category', - select: 'name description' - }) - .sort('createdAt') - .gt('createdAt', data.createdAt) - .exec() - .catch(err => { - ctx.log.error('后一篇文章获取失败,err:', err.message) - return null - }) - data.adjacent = { - prev: prev && prev.toObject() || null, - next: next && next.toObject() || null - } - } + if (data && data._id) { + const query = {} + // 如果未通过权限校验,将文章状态重置为1 + if (!ctx._isAuthenticated) { + query.state = 1 + } + const prev = await articleProxy.findOne(query) + .select('title createdAt publishedAt thumb category') + .populate({ + path: 'category', + select: 'name description' + }) + .sort('-createdAt') + .lt('createdAt', data.createdAt) + .exec() + .catch(err => { + ctx.log.error('前一篇文章获取失败,err:', err.message) + return null + }) + const next = await articleProxy.findOne(query) + .select('title createdAt publishedAt thumb category') + .populate({ + path: 'category', + select: 'name description' + }) + .sort('createdAt') + .gt('createdAt', data.createdAt) + .exec() + .catch(err => { + ctx.log.error('后一篇文章获取失败,err:', err.message) + return null + }) + data.adjacent = { + prev: prev ? prev.toObject() : null, + next: next ? next.toObject() : null + } + } } diff --git a/server/controller/auth.js b/server/controller/auth.js index d0846c5..fd131aa 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -6,94 +6,93 @@ 'use strict' -const jwt = require('jsonwebtoken') -const passport = require('koa-passport') +// const passport = require('koa-passport') const config = require('../config') const { UserModel } = require('../model') -const { bhash, bcompare, getDebug, signToken, proxy, randomString } = require('../util') +const { bcompare, getDebug, signToken, proxy, randomString } = require('../util') const debug = getDebug('Auth') const { getGithubToken, getGithubAuthUserInfo } = require('../service') -const debugGithub = getDebug('Github:Auth') -const isProd = process.env.NODE_ENV === 'production' +// const debugGithub = getDebug('Github:Auth') +// const isProd = process.env.NODE_ENV === 'production' exports.localLogin = async (ctx, next) => { - const name = ctx.validateBody('name') - .required('the "name" parameter is required') - .notEmpty() - .isString('the "name" parameter should be String type') - .val() - const password = ctx.validateBody('password') - .required('the "password" parameter is required') - .notEmpty() - .isString('the "password" parameter should be String type') - .val() - - const user = await UserModel.findOne({ name }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (user) { - const vertifyPassword = bcompare(password, user.password) - if (vertifyPassword) { - const { session } = config.auth - const token = signToken({ - id: user._id, - name: user.name - }) - ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - ctx.cookies.set(config.auth.userCookieKey, user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - debug.success('登录成功, 用户ID:%s,用户名:%s', user._id, user.name) - ctx.success({ - id: user._id, - token - }, 'login success') - } else { - ctx.fail(-1, 'incorrect password') - } - } else { - ctx.fail(-1, 'user doesn\'t exist') - } + const name = ctx.validateBody('name') + .required('the "name" parameter is required') + .notEmpty() + .isString('the "name" parameter should be String type') + .val() + const password = ctx.validateBody('password') + .required('the "password" parameter is required') + .notEmpty() + .isString('the "password" parameter should be String type') + .val() + + const user = await UserModel.findOne({ name }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (user) { + const vertifyPassword = bcompare(password, user.password) + if (vertifyPassword) { + const { session } = config.auth + const token = signToken({ + id: user._id, + name: user.name + }) + ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) + ctx.cookies.set(config.auth.userCookieKey, user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) + debug.success('登录成功, 用户ID:%s,用户名:%s', user._id, user.name) + ctx.success({ + id: user._id, + token + }, 'login success') + } else { + ctx.fail(-1, 'incorrect password') + } + } else { + ctx.fail(-1, 'user doesn\'t exist') + } } exports.logout = async (ctx, next) => { - const { session } = config.auth - const token = signToken({ - id: ctx._user._id, - name: ctx._user.name - }, false) - ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) - ctx.cookies.set(config.auth.userCookieKey, ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) - debug.success('登出成功, 用户ID:%s,用户名:%s', user._id, user.name) - ctx.success(null, 'logout success') + const { session } = config.auth + const token = signToken({ + id: ctx._user._id, + name: ctx._user.name + }, false) + ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) + ctx.cookies.set(config.auth.userCookieKey, ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) + debug.success('登出成功, 用户ID:%s,用户名:%s', ctx.user._id, ctx.user.name) + ctx.success(null, 'logout success') } exports.info = async (ctx, next) => { - const adminId = ctx._user._id - if (!adminId && !ctx._isSnsAuthenticated && !ctx._isAuthenticated) { - return ctx.fail(401) - } - let data = null - if (ctx._isSnsAuthenticated) { - // TODO: 第三方信息获取 - } else if (ctx._isAuthenticated) { - data = await UserModel.findById(adminId) - .select('-password') - .exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - } - - if (data) { - ctx.success({ - info: data, - token: ctx.session._token - }) - } else { - ctx.fail(401) - } + const adminId = ctx._user._id + if (!adminId && !ctx._isSnsAuthenticated && !ctx._isAuthenticated) { + return ctx.fail(401) + } + let data = null + if (ctx._isSnsAuthenticated) { + // TODO: 第三方信息获取 + } else if (ctx._isAuthenticated) { + data = await UserModel.findById(adminId) + .select('-password') + .exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + } + + if (data) { + ctx.success({ + info: data, + token: ctx.session._token + }) + } else { + ctx.fail(401) + } } // github login @@ -116,70 +115,70 @@ exports.info = async (ctx, next) => { // } exports.fetchGithubToken = async (ctx, next) => { - const code = ctx.validateQuery('code').required('缺少code参数').toString().val() - const token = await getGithubToken(code) - if (token) { - ctx.success(token) - } else { - ctx.fail('Token获取失败') - } + const code = ctx.validateQuery('code').required('缺少code参数').toString().val() + const token = await getGithubToken(code) + if (token) { + ctx.success(token) + } else { + ctx.fail('Token获取失败') + } } exports.fetchGithubUser = async (ctx, next) => { - const accessToken = ctx.validateQuery('access_token').required('缺少access_token参数').toString().val() - const data = await getGithubAuthUserInfo(accessToken) - if (!data) { - return ctx.fail('用户信息获取失败') - } - const user = await createLocalUserFromGithub(data) - if (user) { - ctx.success(user) - } else { - ctx.fail('用户信息获取失败') - } + const accessToken = ctx.validateQuery('access_token').required('缺少access_token参数').toString().val() + const data = await getGithubAuthUserInfo(accessToken) + if (!data) { + return ctx.fail('用户信息获取失败') + } + const user = await createLocalUserFromGithub(data) + if (user) { + ctx.success(user) + } else { + ctx.fail('用户信息获取失败') + } } async function createLocalUserFromGithub (githubUser) { - const user = await UserModel.findOne({ - 'github.id': githubUser.id - }).catch(err => { - debug.error('本地用户查找失败, 错误:', err.message) - return null - }) - if (user) { - const userData = { - name: githubUser.name || githubUser.login, - avatar: proxy(githubUser.avatar_url), - slogan: githubUser.bio, - github: githubUser, - role: user.role - } - const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData) - .select('-password -role -createdAt -updatedAt') - .exec().catch(err => { - debug.error('本地用户更新失败, 错误:', err.message) - }) || user - - return updatedUser.toObject() - } else { - const newUser = { - name: githubUser.name || githubUser.login, - avatar: proxy(githubUser.avatar_url), - slogan: githubUser.bio, - github: githubUser, - role: 1 - } - - const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { - debug.error('本地用户查找失败, 错误:', err.message) - return true - }) - - if (checkUser) { - newUser.name += '-' + randomString() - } - - const data = await new UserModel(newUser).save().catch(err => debug.error('本地用户创建失败, 错误:', err.message)) - return data && data.toObject() || null - } + const user = await UserModel.findOne({ + 'github.id': githubUser.id + }).catch(err => { + debug.error('本地用户查找失败, 错误:', err.message) + return null + }) + if (user) { + const userData = { + name: githubUser.name || githubUser.login, + avatar: proxy(githubUser.avatar_url), + slogan: githubUser.bio, + github: githubUser, + role: user.role + } + const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData) + .select('-password -role -createdAt -updatedAt') + .exec().catch(err => { + debug.error('本地用户更新失败, 错误:', err.message) + }) || user + + return updatedUser.toObject() + } else { + const newUser = { + name: githubUser.name || githubUser.login, + avatar: proxy(githubUser.avatar_url), + slogan: githubUser.bio, + github: githubUser, + role: 1 + } + + const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { + debug.error('本地用户查找失败, 错误:', err.message) + return true + }) + + if (checkUser) { + newUser.name += '-' + randomString() + } + + const data = await new UserModel(newUser).save().catch(err => debug.error('本地用户创建失败, 错误:', err.message)) + return data ? data.toObject() : null + } } diff --git a/server/controller/category.js b/server/controller/category.js index 3e24761..5ed60d8 100644 --- a/server/controller/category.js +++ b/server/controller/category.js @@ -10,125 +10,125 @@ const { articleProxy, categoryProxy } = require('../proxy') // 分类列表 exports.list = async (ctx, next) => { - const keyword = ctx.validateQuery('keyword').optional().toString().val() - // 是否按照list属性排序 - const rank = ctx.validateQuery('rank').defaultTo(1).toInt().isIn([0, 1], 'rank参数错误').val() - - const query = {} - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { name: keywordReg } - ] - } - - let sort = '-createdAt' - if (rank) { - sort = 'list ' + sort - } - - const data = await categoryProxy.find(query).sort(sort) - - if (data) { - for (let i = 0; i < data.length; i++) { - if (typeof data[i].toObject === 'function') { - data[i] = data[i].toObject() - } - const articles = await articleProxy.find({ category: data[i]._id }).exec().catch(err => { - ctx.log.error(err.message) - return [] - }) - data[i].count = articles.length - } - ctx.success(data, '分类列表获取成功') - } else { - ctx.fail('分类列表获取失败') - } + const keyword = ctx.validateQuery('keyword').optional().toString().val() + // 是否按照list属性排序 + const rank = ctx.validateQuery('rank').defaultTo(1).toInt().isIn([0, 1], 'rank参数错误').val() + + const query = {} + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { name: keywordReg } + ] + } + + let sort = '-createdAt' + if (rank) { + sort = 'list ' + sort + } + + const data = await categoryProxy.find(query).sort(sort) + + if (data) { + for (let i = 0; i < data.length; i++) { + if (typeof data[i].toObject === 'function') { + data[i] = data[i].toObject() + } + const articles = await articleProxy.find({ category: data[i]._id }).exec().catch(err => { + ctx.log.error(err.message) + return [] + }) + data[i].count = articles.length + } + ctx.success(data, '分类列表获取成功') + } else { + ctx.fail('分类列表获取失败') + } } // 分类详情 exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() - - let data = await categoryProxy.getById(id).exec() - - if (data) { - data = data.toObject() - const articles = await articleProxy.find({ category: id }) - .select('-category') - .exec() - .catch(err => { - ctx.log.error(err.message) - return [] - }) - data.articles = articles - data.articles_count = articles.length - ctx.success(data, '分类详情获取成功') - } else { - ctx.fail('分类详情获取失败') - } + const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() + + let data = await categoryProxy.getById(id).exec() + + if (data) { + data = data.toObject() + const articles = await articleProxy.find({ category: id }) + .select('-category') + .exec() + .catch(err => { + ctx.log.error(err.message) + return [] + }) + data.articles = articles + data.articles_count = articles.length + ctx.success(data, '分类详情获取成功') + } else { + ctx.fail('分类详情获取失败') + } } // 分类创建 exports.create = async (ctx, next) => { - const name = ctx.validateBody('name').required('缺少分类名称').notEmpty().val() - const description = ctx.validateBody('description').optional().val() - const list = ctx.validateBody('list').defaultTo(1).toInt().val() - const ext = ctx.validateBody('extends').optional().toArray().val() - - const { length } = await categoryProxy.find({ name }).exec().catch(err => { - ctx.log.error(err.message) - return [] - }) - - if (!length) { - const data = await categoryProxy.newAndSave({ - name, - description, - extends: ext, - list - }) - - data && data.length - ? ctx.success(data, '分类创建成功') - : ctx.fail('分类创建失败') - } else { - ctx.fail(`【${name}】分类已经存在`) - } + const name = ctx.validateBody('name').required('缺少分类名称').notEmpty().val() + const description = ctx.validateBody('description').optional().val() + const list = ctx.validateBody('list').defaultTo(1).toInt().val() + const ext = ctx.validateBody('extends').optional().toArray().val() + + const { length } = await categoryProxy.find({ name }).exec().catch(err => { + ctx.log.error(err.message) + return [] + }) + + if (!length) { + const data = await categoryProxy.newAndSave({ + name, + description, + extends: ext, + list + }) + + data && data.length + ? ctx.success(data, '分类创建成功') + : ctx.fail('分类创建失败') + } else { + ctx.fail(`【${name}】分类已经存在`) + } } // 分类更新 exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() - const name = ctx.validateBody('name').optional().val() - const description = ctx.validateBody('description').optional().val() - const list = ctx.validateBody('list').optional().toInt().val() - const category = {} + const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() + const name = ctx.validateBody('name').optional().val() + const description = ctx.validateBody('description').optional().val() + const list = ctx.validateBody('list').optional().toInt().val() + const category = {} - name && (category.name = name) - description && (category.description = description) - list && (category.list = list) + name && (category.name = name) + description && (category.description = description) + list && (category.list = list) - const data = await categoryProxy.updateById(id, category).exec() + const data = await categoryProxy.updateById(id, category).exec() - data - ? ctx.success(data, '分类更新成功') - : ctx.fail('分类更新失败') + data + ? ctx.success(data, '分类更新成功') + : ctx.fail('分类更新失败') } // 删除分类 exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() - const articles = await articleProxy.find({ category: id }).exec() - - if (articles && articles.length) { - // 分类下面有文章,不能删除 - ctx.fail('该分类下有文章,不能删除') - } else { - const data = await categoryProxy.delById(id).exec() - data && data.result && data.result.ok - ? ctx.success(null, '分类删除成功') - : ctx.fail('分类删除失败') - } + const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() + const articles = await articleProxy.find({ category: id }).exec() + + if (articles && articles.length) { + // 分类下面有文章,不能删除 + ctx.fail('该分类下有文章,不能删除') + } else { + const data = await categoryProxy.delById(id).exec() + data && data.result && data.result.ok + ? ctx.success(null, '分类删除成功') + : ctx.fail('分类删除失败') + } } diff --git a/server/controller/comment.js b/server/controller/comment.js index c788323..c3276e7 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -14,650 +14,642 @@ const debug = getDebug('Comment') const isProd = process.env.NODE_ENV === 'development' exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.commentLimit).toInt().gt(0, '每页评论数量必须大于0').val() - const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() - const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() - const type = ctx.validateQuery('type').optional().toInt().isIn([0, 1], '评论类型参数无效').val() - const author = ctx.validateQuery('author').optional().toString().isObjectId('用户ID参数无效').val() - const article = ctx.validateQuery('article').optional().toString().isObjectId('文章ID参数无效').val() - const keyword = ctx.validateQuery('keyword').optional().toString().val() - const parent = ctx.validateQuery('parent').optional().toString().isObjectId('父评论ID参数无效').val() - // 时间区间查询仅后台可用,且依赖于createdAt - const startDate = ctx.validateQuery('start_date').optional().toString().val() - const endDate = ctx.validateQuery('end_date').optional().toString().val() - // 排序仅后台能用,且order和sortBy需同时传入才起作用 - // -1 desc | 1 asc - const order = ctx.validateQuery('order').optional().toInt().isIn([-1, 1], '排序方式参数无效').val() - // createdAt | updatedAt | ups - const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn(['createdAt', 'updatedAt', 'ups'], '排序项参数无效').val() - - // 过滤条件 - const options = { - sort: { createdAt: 1 }, - page, - limit: pageSize, - select: '', - populate: [ - { - path: 'author', - select: !ctx._isAuthenticated ? 'github avatar name site' : '' - }, - { - path: 'parent', - select: 'author meta sticky ups', - match: { - state: 1 - } - }, - { - path: 'forward', - select: 'author meta sticky ups', - match: { - state: 1 - }, - populate: { - path: 'author', - select: 'avatar github name' - } - } - ] - } - - // 查询条件 - const query = {} - - if (type !== undefined) { - query.type = type - } - - if (state !== undefined) { - query.state = state - } - - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { content: keywordReg } - ] - } - - // 用户 - if (author) { - // 如果是id - if (isObjectId(author)) { - query.author = author - } else { - // 普通字符串,需要先查到id - const u = await UserModel.findOne({ name: author }).exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - query.author = u ? u._id : createObjectId() - } - } - - // 文章 - if (article) { - // 如果是id - if (isObjectId(article)) { - query.article = article - } else { - // 普通字符串,需要先查到id - const a = await ArticleModel.findOne({ name: article }).exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - query.article = a ? a._id : createObjectId() - } - } - - // 排序 - if (sortBy && order) { - options.sort = {} - options.sort[sortBy] = order - } - - if (parent) { - // 获取子评论 - query.parent = parent - } else { - // 获取父评论 - query.parent = { $exists: false } - } - - // 未通过权限校验(前台获取评论列表) - if (!ctx._isAuthenticated) { - // 将评论状态重置为1 - query.state = 1 - query.spam = false - // 评论列表不需要content和state - options.select = '-content -state -updatedAt -spam -type' - } else { - // 起始日期 - if (startDate) { - const $gte = new Date(startDate) - if ($gte.toString() !== 'Invalid Date') { - query.createdAt = { $gte } - } - } - - // 结束日期 - if (endDate) { - const $lte = new Date(endDate) - if ($lte.toString() !== 'Invalid Date') { - query.createdAt = Object.assign({}, query.createdAt, { $lte }) - } - } - } - - const comments = await CommentModel.paginate(query, options).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (comments) { - const data = [] - // 查询子评论数量 - await Promise.all(comments.docs.map(doc => { - doc = doc.toObject() - doc.subCount = 0 - data.push(doc) - return CommentModel.count({ parent: doc._id }).exec() - .then(count => { - doc.subCount = count - }) - .catch(err => { - ctx.log.error(err) - doc.subCount = 0 - }) - })) - ctx.success({ - list: data, - pagination: { - total: comments.total, - current_page: comments.page > comments.pages ? comments.pages : comments.page, - total_page: comments.pages, - per_page: comments.limit - } - }) - } else { - ctx.fail(-1) - } + const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.commentLimit).toInt().gt(0, '每页评论数量必须大于0').val() + const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() + const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() + const type = ctx.validateQuery('type').optional().toInt().isIn([0, 1], '评论类型参数无效').val() + const author = ctx.validateQuery('author').optional().toString().isObjectId('用户ID参数无效').val() + const article = ctx.validateQuery('article').optional().toString().isObjectId('文章ID参数无效').val() + const keyword = ctx.validateQuery('keyword').optional().toString().val() + const parent = ctx.validateQuery('parent').optional().toString().isObjectId('父评论ID参数无效').val() + // 时间区间查询仅后台可用,且依赖于createdAt + const startDate = ctx.validateQuery('start_date').optional().toString().val() + const endDate = ctx.validateQuery('end_date').optional().toString().val() + // 排序仅后台能用,且order和sortBy需同时传入才起作用 + // -1 desc | 1 asc + const order = ctx.validateQuery('order').optional().toInt().isIn([-1, 1], '排序方式参数无效').val() + // createdAt | updatedAt | ups + const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn(['createdAt', 'updatedAt', 'ups'], '排序项参数无效').val() + + // 过滤条件 + const options = { + sort: { createdAt: 1 }, + page, + limit: pageSize, + select: '', + populate: [ + { + path: 'author', + select: !ctx._isAuthenticated ? 'github avatar name site' : '' + }, + { + path: 'parent', + select: 'author meta sticky ups', + match: { + state: 1 + } + }, + { + path: 'forward', + select: 'author meta sticky ups', + match: { + state: 1 + }, + populate: { + path: 'author', + select: 'avatar github name' + } + } + ] + } + + // 查询条件 + const query = {} + + if (type !== undefined) { + query.type = type + } + + if (state !== undefined) { + query.state = state + } + + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { content: keywordReg } + ] + } + + // 用户 + if (author) { + // 如果是id + if (isObjectId(author)) { + query.author = author + } else { + // 普通字符串,需要先查到id + const u = await UserModel.findOne({ name: author }).exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + query.author = u ? u._id : createObjectId() + } + } + + // 文章 + if (article) { + // 如果是id + if (isObjectId(article)) { + query.article = article + } else { + // 普通字符串,需要先查到id + const a = await ArticleModel.findOne({ name: article }).exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + query.article = a ? a._id : createObjectId() + } + } + + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } + + if (parent) { + // 获取子评论 + query.parent = parent + } else { + // 获取父评论 + query.parent = { $exists: false } + } + + // 未通过权限校验(前台获取评论列表) + if (!ctx._isAuthenticated) { + // 将评论状态重置为1 + query.state = 1 + query.spam = false + // 评论列表不需要content和state + options.select = '-content -state -updatedAt -spam -type' + } else { + // 起始日期 + if (startDate) { + const $gte = new Date(startDate) + if ($gte.toString() !== 'Invalid Date') { + query.createdAt = { $gte } + } + } + + // 结束日期 + if (endDate) { + const $lte = new Date(endDate) + if ($lte.toString() !== 'Invalid Date') { + query.createdAt = Object.assign({}, query.createdAt, { $lte }) + } + } + } + + const comments = await CommentModel.paginate(query, options).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (comments) { + const data = [] + // 查询子评论数量 + await Promise.all(comments.docs.map(doc => { + doc = doc.toObject() + doc.subCount = 0 + data.push(doc) + return CommentModel.count({ parent: doc._id }).exec() + .then(count => { + doc.subCount = count + }) + .catch(err => { + ctx.log.error(err) + doc.subCount = 0 + }) + })) + ctx.success({ + list: data, + pagination: { + total: comments.total, + current_page: comments.page > comments.pages ? comments.pages : comments.page, + total_page: comments.pages, + per_page: comments.limit + } + }) + } else { + ctx.fail(-1) + } } exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() - - let data = null - let queryPs = null - if (!ctx._isAuthenticated) { - queryPs = CommentModel.findById(id, { state: 1, spam: false }) - .select('-content -state -updatedAt -type -spam') - .populate({ - path: 'author', - select: 'github' - }) - .populate({ - path: 'parent', - select: 'author meta sticky ups' - }) - .populate({ - path: 'forward', - select: 'author meta sticky ups' - }) - } else { - queryPs = CommentModel.findById(id) - } - - data = await queryPs.exec().catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - data = data.toObject() - ctx.success(data) - } else { - ctx.fail('评论不存在') - } + const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + + let data = null + let queryPs = null + if (!ctx._isAuthenticated) { + queryPs = CommentModel.findById(id, { state: 1, spam: false }) + .select('-content -state -updatedAt -type -spam') + .populate({ + path: 'author', + select: 'github' + }) + .populate({ + path: 'parent', + select: 'author meta sticky ups' + }) + .populate({ + path: 'forward', + select: 'author meta sticky ups' + }) + } else { + queryPs = CommentModel.findById(id) + } + + data = await queryPs.exec().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + data = data.toObject() + ctx.success(data) + } else { + ctx.fail('评论不存在') + } } exports.create = async (ctx, next) => { - const content = ctx.validateBody('content') - .required('内容参数必填') - .notEmpty() - .isString('内容参数必须是字符串类型') - .val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() - const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], '置顶参数无效').val() - const type = ctx.validateBody('type').defaultTo(0).toInt().isIn([0, 1], '评论类型参数无效').val() - const article = ctx.validateBody('article').optional().toString().isObjectId('文章ID参数无效').val() - const parent = ctx.validateBody('parent').optional().toString().isObjectId('父评论ID参数无效').val() - const forward = ctx.validateBody('forward').optional().toString().isObjectId('前置评论ID参数无效').val() - // ObjectId | { id, name, email, site } - const author = ctx.validateBody('author').required('作者参数无效').val() - const req = ctx.req - const comment = { content } - - if (type === undefined || type === 0) { - if (!article) { - return ctx.fail('缺少文章ID参数') - } - comment.article = article - } - - if (parent && !forward || !parent && forward) { - return ctx.fail('父评论ID和前置评论ID必须同时存在') - } - - const user = await checkAuthor.call(ctx, author) - if (!user) { - return ctx.fail('作者不存在') - } else if (user.mute) { - // 如果被禁言 - return ctx.fail('您已经被禁言') - } - comment.author = user._id - - if (!checkUserSpam(user)) { - return ctx.fail('您的垃圾评论数量已达到最大限制,已被禁言') - } - - if (state !== undefined) { - comment.state = state - } - - if (type !== undefined) { - comment.type = type - } - - if (sticky !== undefined) { - comment.sticky = sticky - } - - const { ip, location } = getLocation(req) - comment.meta = {} - comment.meta.location = location || null - comment.meta.ip = ip - comment.meta.ua = req.headers['user-agent'] || '' - comment.meta.referer = req.headers.referer || '' - - // 先判断是不是垃圾邮件 - const akismetClient = akismet.getAkismetClient() - let isSpam = false - // 永链 - const permalink = getPermalink(comment) - if (akismetClient) { - isSpam = await akismetClient.checkSpam({ - user_ip : ip, // Required! - user_agent : comment.meta.ua, // Required! - referrer : comment.meta.referer, // Required! - permalink, - comment_type : getCommentType(type), - comment_author : user.name, - comment_author_email : user.email, - comment_author_url : user.site, - comment_content : content, - is_test : isProd - }) - } - - // 如果是Spam评论 - if (isSpam) { - return ctx.fail('检测为垃圾评论,该评论将不会显示') - } - - parent && (comment.parent = parent) - forward && (comment.forward = forward) - comment.renderedContent = marked(content) - - let data = await new CommentModel(comment).save().catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - let p = CommentModel.findById(data._id) - if (!ctx._isAuthenticated) { - p = p.select('-content -state -updatedAt') - .populate({ - path: 'author', - select: 'name site avatar role mute email' - }) - .populate({ - path: 'parent', - select: 'author meta sticky ups' - }) - .populate({ - path: 'forward', - select: 'author meta sticky ups' - }) - } - data = await p - .exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - ctx.success(data, '评论成功') - // 如果是文章评论,则更新文章评论数量 - if (type === 0) { - updateArticleCommentCount([comment.article]) - } - // 发送邮件通知站主和被评论者 - sendEmailToAdminAndUser(data, permalink) - } else { - ctx.fail('评论失败') - } + const content = ctx.validateBody('content').required('内容参数必填').notEmpty().val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() + const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], '置顶参数无效').val() + const type = ctx.validateBody('type').defaultTo(0).toInt().isIn([0, 1], '评论类型参数无效').val() + const article = ctx.validateBody('article').optional().toString().isObjectId('文章ID参数无效').val() + const parent = ctx.validateBody('parent').optional().toString().isObjectId('父评论ID参数无效').val() + const forward = ctx.validateBody('forward').optional().toString().isObjectId('前置评论ID参数无效').val() + // ObjectId | { id, name, email, site } + const author = ctx.validateBody('author').required('作者参数无效').val() + const req = ctx.req + const comment = { content } + + if (type === undefined || type === 0) { + if (!article) { + return ctx.fail('缺少文章ID参数') + } + comment.article = article + } + + if ((parent && !forward) || (!parent && forward)) { + return ctx.fail('父评论ID和前置评论ID必须同时存在') + } + + const user = await checkAuthor.call(ctx, author) + if (!user) { + return ctx.fail('作者不存在') + } else if (user.mute) { + // 如果被禁言 + return ctx.fail('您已经被禁言') + } + comment.author = user._id + + if (!checkUserSpam(user)) { + return ctx.fail('您的垃圾评论数量已达到最大限制,已被禁言') + } + + if (state !== undefined) { + comment.state = state + } + + if (type !== undefined) { + comment.type = type + } + + if (sticky !== undefined) { + comment.sticky = sticky + } + + const { ip, location } = getLocation(req) + comment.meta = {} + comment.meta.location = location || null + comment.meta.ip = ip + comment.meta.ua = req.headers['user-agent'] || '' + comment.meta.referer = req.headers.referer || '' + + // 先判断是不是垃圾邮件 + const akismetClient = akismet.getAkismetClient() + let isSpam = false + // 永链 + const permalink = getPermalink(comment) + if (akismetClient) { + isSpam = await akismetClient.checkSpam({ + user_ip: ip, + user_agent: comment.meta.ua, + referrer: comment.meta.referer, + permalink, + comment_type: getCommentType(type), + comment_author: user.name, + comment_author_email: user.email, + comment_author_url: user.site, + comment_content: content, + is_test: isProd + }) + } + + // 如果是Spam评论 + if (isSpam) { + return ctx.fail('检测为垃圾评论,该评论将不会显示') + } + + parent && (comment.parent = parent) + forward && (comment.forward = forward) + comment.renderedContent = marked(content) + + let data = await new CommentModel(comment).save().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + let p = CommentModel.findById(data._id) + if (!ctx._isAuthenticated) { + p = p.select('-content -state -updatedAt') + .populate({ + path: 'author', + select: 'name site avatar role mute email' + }) + .populate({ + path: 'parent', + select: 'author meta sticky ups' + }) + .populate({ + path: 'forward', + select: 'author meta sticky ups' + }) + } + data = await p + .exec() + .catch(err => { + ctx.log.error(err.message) + return null + }) + ctx.success(data, '评论成功') + // 如果是文章评论,则更新文章评论数量 + if (type === 0) { + updateArticleCommentCount([comment.article]) + } + // 发送邮件通知站主和被评论者 + sendEmailToAdminAndUser(data, permalink) + } else { + ctx.fail('评论失败') + } } exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() - const content = ctx.validateBody('content').optional().isString('内容参数必须是字符串类型').val() - const state = ctx.validateBody('state').optional().toInt().isIn([-2, 0, 1, 2], '评论状态参数无效').val() - const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], '置顶参数无效').val() - const comment = {} - let cache = await CommentModel.findById(id) - .populate('author') - .exec() - if (!cache) { - return ctx.fail('评论不存在') - } - cache = cache.toObject() - if (ctx._isAuthenticated && ctx._user._id.toString() !== cache.author._id.toString()) { - return ctx.fail('其他人的评论内容不能修改') - } - - if (content !== undefined) { - comment.content = content - comment.renderedContent = marked(content) - } - - if (sticky !== undefined) { - comment.sticky = sticky - } - - // 状态修改是涉及到spam修改 - if (state !== undefined) { - comment.state = state - const akismetClient = akismet.getAkismetClient() - const permalink = getPermalink(cache) - const opt = { - user_ip : cache.meta.ip, // Required! - user_agent : cache.meta.ua, // Required! - referrer : cache.meta.referer, // Required! - permalink, - comment_type : getCommentType(cache.type), - comment_author : cache.author.github.login, - comment_author_email : cache.author.github.email, - comment_author_url : cache.author.github.blog, - comment_content : cache.content, - is_test : isProd - } - - if (cache.state === -2 && state !== -2) { - // 垃圾评论转为正常评论 - if (cache.spam) { - comment.spam = false - // 报告给Akismet - akismetClient.submitSpam(opt) - } - } else if (cache.state !== -2 && state === -2) { - // 正常评论转为垃圾评论 - if (!cache.spam) { - comment.spam = true - // 报告给Akismet - akismetClient.submitHam(opt) - } - } - } - - let p = CommentModel.findByIdAndUpdate(id, comment, { new: true }) - if (!ctx._isAuthenticated) { - p = p.select('-content -state -updatedAt') - .populate({ - path: 'author', - select: 'github' - }) - .populate({ - path: 'parent', - select: 'author meta sticky ups' - }) - .populate({ - path: 'forward', - select: 'author meta sticky ups' - }) - } - const data = await p.exec().catch(err => { - ctx.log.error(err.message) - return null - }) - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + const content = ctx.validateBody('content').optional().isString('内容参数必须是字符串类型').val() + const state = ctx.validateBody('state').optional().toInt().isIn([-2, 0, 1, 2], '评论状态参数无效').val() + const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], '置顶参数无效').val() + const comment = {} + let cache = await CommentModel.findById(id) + .populate('author') + .exec() + if (!cache) { + return ctx.fail('评论不存在') + } + cache = cache.toObject() + if (ctx._isAuthenticated && ctx._user._id.toString() !== cache.author._id.toString()) { + return ctx.fail('其他人的评论内容不能修改') + } + + if (content !== undefined) { + comment.content = content + comment.renderedContent = marked(content) + } + + if (sticky !== undefined) { + comment.sticky = sticky + } + + // 状态修改是涉及到spam修改 + if (state !== undefined) { + comment.state = state + const akismetClient = akismet.getAkismetClient() + const permalink = getPermalink(cache) + const opt = { + user_ip: cache.meta.ip, + user_agent: cache.meta.ua, + referrer: cache.meta.referer, + permalink, + comment_type: getCommentType(cache.type), + comment_author: cache.author.github.login, + comment_author_email: cache.author.github.email, + comment_author_url: cache.author.github.blog, + comment_content: cache.content, + is_test: isProd + } + + if (cache.state === -2 && state !== -2) { + // 垃圾评论转为正常评论 + if (cache.spam) { + comment.spam = false + // 报告给Akismet + akismetClient.submitSpam(opt) + } + } else if (cache.state !== -2 && state === -2) { + // 正常评论转为垃圾评论 + if (!cache.spam) { + comment.spam = true + // 报告给Akismet + akismetClient.submitHam(opt) + } + } + } + + let p = CommentModel.findByIdAndUpdate(id, comment, { new: true }) + if (!ctx._isAuthenticated) { + p = p.select('-content -state -updatedAt') + .populate({ + path: 'author', + select: 'github' + }) + .populate({ + path: 'parent', + select: 'author meta sticky ups' + }) + .populate({ + path: 'forward', + select: 'author meta sticky ups' + }) + } + const data = await p.exec().catch(err => { + ctx.log.error(err.message) + return null + }) + if (data) { + ctx.success(data) + } else { + ctx.fail() + } } exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() - const data = await CommentModel.remove({ _id: id }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data && data.result && data.result.ok) { - ctx.success() - } else { - ctx.fail('评论不存在') - } + const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + const data = await CommentModel.remove({ _id: id }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data && data.result && data.result.ok) { + ctx.success() + } else { + ctx.fail('评论不存在') + } } exports.like = async (ctx, next) => { - const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() - const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() - - const data = await CommentModel.findByIdAndUpdate(id, { - $inc: { - ups: like ? 1 : -1 - } - }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - ctx.success() - } else { - ctx.fail('评论不存在') - } + const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() + + const data = await CommentModel.findByIdAndUpdate(id, { + $inc: { + ups: like ? 1 : -1 + } + }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success() + } else { + ctx.fail('评论不存在') + } } // 获取永久链接 function getPermalink (comment = {}) { - const { type, article } = comment - switch (type) { - case 0: - return `${config.site}/blog/article/${article}` - case 1: - return `${config.site}/guestbook` - default: - return '' - break - } + const { type, article } = comment + switch (type) { + case 0: + return `${config.site}/blog/article/${article}` + case 1: + return `${config.site}/guestbook` + default: + return '' + } } // 评论类型说明 function getCommentType (type) { - switch (type) { - case 0: - return '文章评论' - break - case 1: - return '站点留言' - default: - return '评论' - break - } + switch (type) { + case 0: + return '文章评论' + case 1: + return '站点留言' + default: + return '评论' + } } // 检测用户以往spam评论 async function checkUserSpam (user) { - const userComments = await CommentModel.find({ - author: user._id - }) - .exec() - .catch(err => { - debug.error('用户历史评论获取失败,错误:', err.message) - return [] - }) - - const spamComments = userComments.filter(c => c.spam) - // 如果用户以往评论中spam评论数量大于等于spam限制 - if (spamComments.length >= config.limit.commentSpamLimit) { - if (!user.mute) { - // 将用户禁言 - await UserModel.update({ _id: user._id }, { - mute: true - }) - .exec() - .then(() => { - debug.success('用户禁言成功,用户:', user.name) - }) - .catch(err => { - debug.error('用户禁言失败,请手动禁言,错误:', err.message) - }) - } - return false - } - return true + const userComments = await CommentModel.find({ + author: user._id + }) + .exec() + .catch(err => { + debug.error('用户历史评论获取失败,错误:', err.message) + return [] + }) + + const spamComments = userComments.filter(c => c.spam) + // 如果用户以往评论中spam评论数量大于等于spam限制 + if (spamComments.length >= config.limit.commentSpamLimit) { + if (!user.mute) { + // 将用户禁言 + await UserModel.update({ _id: user._id }, { + mute: true + }) + .exec() + .then(() => { + debug.success('用户禁言成功,用户:', user.name) + }) + .catch(err => { + debug.error('用户禁言失败,请手动禁言,错误:', err.message) + }) + } + return false + } + return true } // 更新文章的meta.comments评论数量 async function updateArticleCommentCount (articleIds = []) { - if (!articleIds.length) { - return - } - // TIP: 这里必须$in的是一个ObjectId对象数组,而不能只是id字符串数组 - articleIds = [...new Set(articleIds)].filter(id => isObjectId(id)).map(id => createObjectId(id)) - const counts = await CommentModel.aggregate([ - { $match: { state: 1, article: { $in: articleIds } } }, - { $group: { _id: '$article', total_count: { $sum: 1 } } } - ]) - .exec() - .catch(err => { - debug.error('更新文章评论数量前聚合评论数据操作失败,错误:', err.message) - return [] - }) - Promise.all(counts.map(count => ArticleModel.update( - { _id: count._id }, - { $set: { 'meta.comments': count.total_count } } - ).exec().catch(err => { - debug.error('文章评论数量更新失败,错误:', err.message) - }))).then(() => { - debug.success('文章评论数量更新成功') - }) + if (!articleIds.length) { + return + } + // TIP: 这里必须$in的是一个ObjectId对象数组,而不能只是id字符串数组 + articleIds = [...new Set(articleIds)].filter(id => isObjectId(id)).map(id => createObjectId(id)) + const counts = await CommentModel.aggregate([ + { $match: { state: 1, article: { $in: articleIds } } }, + { $group: { _id: '$article', total_count: { $sum: 1 } } } + ]) + .exec() + .catch(err => { + debug.error('更新文章评论数量前聚合评论数据操作失败,错误:', err.message) + return [] + }) + Promise.all(counts.map(count => ArticleModel.update( + { _id: count._id }, + { $set: { 'meta.comments': count.total_count } } + ).exec().catch(err => { + debug.error('文章评论数量更新失败,错误:', err.message) + }))).then(() => { + debug.success('文章评论数量更新成功') + }) } // 发送邮件 async function sendEmailToAdminAndUser (comment, permalink) { - const { type, article } = comment - let adminTitle = '位置的评论' - let adminType = '评论' - if (type === 0) { - // 文章评论 - const at = await ArticleModel.findById(article).exec() - if (at && at._id) { - adminTitle = `博客文章 [${at.title}] 有了新的评论` - } - adminType = '评论' - } else if (type === 1) { - // 站内留言 - adminTitle = `个人站点有新的留言` - adminType = '留言' - } - - // 发送给管理员邮箱config.email - mailer.send({ - subject: adminTitle, - text: `来自 ${comment.author.github.name} 的${adminType}:${comment.content}`, - html: `

来自 ${comment.author.github.name} 的${adminType} [ 点击查看 ]:${comment.renderedContent}

` - }, true) - - // 发送给被评论者 - if (comment.forward) { - const forwardAuthor = await UserModel.findById(comment.forward.author).exec().catch(err => null) - if (forwardAuthor) { - mailer.send({ - to: forwardAuthor.github.email, - subject: '你在 Jooger 的博客的评论有了新的回复', - text: `来自 ${comment.author.name} 的回复:${comment.content}`, - html: `

来自 ${comment.author.name} 的回复 [ 点击查看 ]:${comment.renderedContent}

` - }) - } else { - debug.warn('给被评论者邮件失败') - } - } + const { type, article } = comment + let adminTitle = '位置的评论' + let adminType = '评论' + if (type === 0) { + // 文章评论 + const at = await ArticleModel.findById(article).exec() + if (at && at._id) { + adminTitle = `博客文章 [${at.title}] 有了新的评论` + } + adminType = '评论' + } else if (type === 1) { + // 站内留言 + adminTitle = `个人站点有新的留言` + adminType = '留言' + } + + // 发送给管理员邮箱config.email + mailer.send({ + subject: adminTitle, + text: `来自 ${comment.author.github.name} 的${adminType}:${comment.content}`, + html: `

来自 ${comment.author.github.name} 的${adminType} [ 点击查看 ]:${comment.renderedContent}

` + }, true) + + // 发送给被评论者 + if (comment.forward) { + const forwardAuthor = await UserModel.findById(comment.forward.author).exec().catch(() => null) + if (forwardAuthor) { + mailer.send({ + to: forwardAuthor.github.email, + subject: '你在 Jooger 的博客的评论有了新的回复', + text: `来自 ${comment.author.name} 的回复:${comment.content}`, + html: `

来自 ${comment.author.name} 的回复 [ 点击查看 ]:${comment.renderedContent}

` + }) + } else { + debug.warn('给被评论者邮件失败') + } + } } // 验证作者 async function checkAuthor (author) { - let user = null - if (isObjectId(author)) { - user = await findUser({ - _id: author - }) - } else if (isType(author, 'Object')) { - // 需要创建或更新用户 - const update = {} - author.name && (update.name = author.name) - author.site && (update.site = author.site) - if (author.email) { - update.avatar = gravatar(author.email) - update.email = author.email - } - if (author.id) { - // 更新 - if (isObjectId(author.id)) { - user = await UserModel.findByIdAndUpdate(author.id, update, { - new : true - }).exec().catch(err => { - debug.error('用户更新失败,错误:', err.message) - this.log.error(err.message) - return null - }) - if (user) { - debug.success(`用户【${user.name}】更新成功`) - } - } - } else { - // 创建 - user = await new UserModel({ - ...update, - role: config.constant.roleMap.USER - }) - .save() - .catch(err => { - debug.error('用户创建失败,错误:', err.message) - this.log.error(err.message) - return null - }) - if (user) { - debug.success(`用户【${user.name}】创建成功`) - } - } - } - return user + let user = null + if (isObjectId(author)) { + user = await findUser({ + _id: author + }) + } else if (isType(author, 'Object')) { + // 需要创建或更新用户 + const update = {} + author.name && (update.name = author.name) + author.site && (update.site = author.site) + if (author.email) { + update.avatar = gravatar(author.email) + update.email = author.email + } + if (author.id) { + // 更新 + if (isObjectId(author.id)) { + user = await UserModel.findByIdAndUpdate(author.id, update, { + new: true + }).exec().catch(err => { + debug.error('用户更新失败,错误:', err.message) + this.log.error(err.message) + return null + }) + if (user) { + debug.success(`用户【${user.name}】更新成功`) + } + } + } else { + // 创建 + user = await new UserModel({ + ...update, + role: config.constant.roleMap.USER + }) + .save() + .catch(err => { + debug.error('用户创建失败,错误:', err.message) + this.log.error(err.message) + return null + }) + if (user) { + debug.success(`用户【${user.name}】创建成功`) + } + } + } + return user } async function findUser (query = {}, update) { - const user = await UserModel.findOne(query).select('-password').exec().catch(err => { - debug.error('用户查找失败,错误:', err.message) - ctx.log.error(err.message) - return null - }) - return user + const user = await UserModel.findOne(query).select('-password').exec().catch(err => { + debug.error('用户查找失败,错误:', err.message) + return null + }) + return user } diff --git a/server/controller/moment.js b/server/controller/moment.js index 401877e..e707988 100644 --- a/server/controller/moment.js +++ b/server/controller/moment.js @@ -8,124 +8,116 @@ const config = require('../config') const { MomentModel } = require('../model') -const { getDebug, getLocation } = require('../util') -const debug = getDebug('Moment') +const { getLocation } = require('../util') exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.momentLimit).toInt().gt(0, '每页数量必须大于0').val() - const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() - const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'state参数错误').val() - const keyword = ctx.validateQuery('keyword').optional().toString().val() - - const query = {} - const options = { - page, - limit: pageSize, - sort: { createdAt: -1 } - } - - if (!ctx._isAuthenticated) { - query.state = 1 - } else { - if (state !== undefined) { - query.state = state - } - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { content: keywordReg } - ] - } - } - - const moments = await MomentModel.paginate(query, options).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (moments) { - ctx.success({ - list: moments.docs, - pagination: { - total: moments.total, - current_page: moments.page > moments.pages ? moments.pages : moments.page, - total_page: moments.pages, - per_page: moments.limit - } - }) - } else { - ctx.fail(-1) - } + const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.momentLimit).toInt().gt(0, '每页数量必须大于0').val() + const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() + const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'state参数错误').val() + const keyword = ctx.validateQuery('keyword').optional().toString().val() + + const query = {} + const options = { + page, + limit: pageSize, + sort: { createdAt: -1 } + } + + if (!ctx._isAuthenticated) { + query.state = 1 + } else { + if (state !== undefined) { + query.state = state + } + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { content: keywordReg } + ] + } + } + + const moments = await MomentModel.paginate(query, options).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (moments) { + ctx.success({ + list: moments.docs, + pagination: { + total: moments.total, + current_page: moments.page > moments.pages ? moments.pages : moments.page, + total_page: moments.pages, + per_page: moments.limit + } + }) + } else { + ctx.fail(-1) + } } exports.create = async (ctx, next) => { - const content = ctx.validateBody('content') - .required('内容参数必填') - .notEmpty() - .isString('内容参数必须是字符串类型') - .val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '个人动态状态参数无效').val() - const req = ctx.req - const moment = {} - const { ip, location } = getLocation(req) - - if (state !== undefined) { - moment.state = state - } - moment.location = { ip, ...location } - moment.content = content - - const data = await new MomentModel(moment).save().catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + const content = ctx.validateBody('content').required('缺少内容').notEmpty().val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '个人动态状态参数无效').val() + const req = ctx.req + const moment = {} + const { ip, location } = getLocation(req) + + if (state !== undefined) { + moment.state = state + } + moment.location = { ip, ...location } + moment.content = content + + const data = await new MomentModel(moment).save().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } } exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('个人动态ID参数无效').toString().isObjectId('个人动态ID参数无效').val() - const content = ctx.validateBody('content').optional().isString('内容参数必须是字符串类型').val() - const state = ctx.validateBody('state').optional().toInt().isIn([-2, 0, 1, 2], '个人动态状态参数无效').val() - const req = ctx.req - const moment = {} - - if (state !== undefined) { - moment.state = state - } - content && (moment.content = content) - - const data = await MomentModel.findByIdAndUpdate(id, moment, { - new: true - }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + const id = ctx.validateParam('id').required('个人动态ID参数无效').toString().isObjectId('个人动态ID参数无效').val() + const content = ctx.validateBody('content').optional().isString('内容参数必须是字符串类型').val() + const state = ctx.validateBody('state').optional().toInt().isIn([-2, 0, 1, 2], '个人动态状态参数无效').val() + const moment = {} + + if (state !== undefined) { + moment.state = state + } + content && (moment.content = content) + + const data = await MomentModel.findByIdAndUpdate(id, moment, { + new: true + }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data) { + ctx.success(data) + } else { + ctx.fail() + } } exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('个人动态ID参数无效').toString().isObjectId('个人动态ID参数无效').val() - const data = await MomentModel.remove({ _id: id }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data && data.result && data.result.ok) { - ctx.success() - } else { - ctx.fail() - } + const id = ctx.validateParam('id').required('个人动态ID参数无效').toString().isObjectId('个人动态ID参数无效').val() + const data = await MomentModel.remove({ _id: id }).catch(err => { + ctx.log.error(err.message) + return null + }) + + if (data && data.result && data.result.ok) { + ctx.success() + } else { + ctx.fail() + } } - - diff --git a/server/controller/music.js b/server/controller/music.js index 99e3d35..12eae32 100644 --- a/server/controller/music.js +++ b/server/controller/music.js @@ -9,101 +9,80 @@ const config = require('../config') const { modelUpdate, netease } = require('../service') const { optionProxy } = require('../proxy') -const { proxy, getDebug } = require('../util') +const { proxy } = require('../util') const { redis } = require('../plugins') const isProd = process.env.NODE_ENV === 'production' -const debug = getDebug('Music') exports.list = async (ctx, next) => { - // 后台实时获取 - if (ctx._isAuthenticated) { - const playListId = ctx.validateQuery('play_list_id') - .required('the "play_list_id" parameter is required') - .notEmpty() - .isString('the "play_list_id" parameter should be String type') - .val() - - const data = await modelUpdate.fetchSonglist(playListId) - ctx.success(data) - } else { - const option = await optionProxy.findOne().exec().catch(err => { - ctx.log.error(err.message) - return null - }) - - if (!option || !option.musicId) { - return ctx.fail('歌单未找到') - } - - const playListId = option.musicId - const musicData = await redis.get(config.constant.redisCacheKey.music) - - // hit - if (musicData && musicData.id === playListId) { - return ctx.success(musicData.data || []) - } - - // update cache - const data = await modelUpdate.updateMusicCache(playListId) - ctx.success(data && data.data || {}) - } + // 后台实时获取 + if (ctx._isAuthenticated) { + const playListId = ctx.validateQuery('play_list_id').required('缺少歌单ID').notEmpty().val() + + const data = await modelUpdate.fetchSonglist(playListId) + ctx.success(data) + } else { + const option = await optionProxy.findOne().exec().catch(err => { + ctx.log.error(err.message) + return null + }) + + if (!option || !option.musicId) { + return ctx.fail('歌单未找到') + } + + const playListId = option.musicId + const musicData = await redis.get(config.constant.redisCacheKey.music) + + // hit + if (musicData && musicData.id === playListId) { + return ctx.success(musicData.data || []) + } + + // update cache + const data = await modelUpdate.updateMusicCache(playListId) + ctx.success(data ? data.data : {}) + } } exports.item = async (ctx, next) => { - const songId = ctx.validateParam('song_id') - .required('the "song_id" parameter is required') - .notEmpty() - .isString('the "song_id" parameter should be String type') - .val() + const songId = ctx.validateParam('song_id') + .required('the "song_id" parameter is required') + .notEmpty() + .isString('the "song_id" parameter should be String type') + .val() - const { songs } = await netease.neteaseMusic.song(songId) + const { songs } = await netease.neteaseMusic.song(songId) - ctx.success(songs) + ctx.success(songs) } exports.url = async (ctx, next) => { - const songId = ctx.validateParam('song_id') - .required('the "song_id" parameter is required') - .notEmpty() - .isString('the "song_id" parameter should be String type') - .val() - - const data = await netease.neteaseMusic.url(songId).then(data => { - if (!isProd) { - return data.data || [] - } - if (isType(data.data, 'Array')) { - return data.data.map(item => { - item.url = proxy(item.url) - return item - }) - } - return [] - }) - - ctx.success(data) + const songId = ctx.validateParam('song_id').required('缺少歌曲ID').notEmpty().val() + + const data = await netease.neteaseMusic.url(songId).then(data => { + if (!isProd) { + return data.data || [] + } + if (Array.isArray(data.data)) { + return data.data.map(item => { + item.url = proxy(item.url) + return item + }) + } + return [] + }) + + ctx.success(data) } exports.lyric = async (ctx, next) => { - const songId = ctx.validateParam('song_id') - .required('the "song_id" parameter is required') - .notEmpty() - .isString('the "song_id" parameter should be String type') - .val() - - const data = await netease.neteaseMusic.lyric(songId) - - ctx.success(data) + const songId = ctx.validateParam('song_id').required('缺少歌曲ID').notEmpty().val() + const data = await netease.neteaseMusic.lyric(songId) + ctx.success(data) } exports.cover = async (ctx, next) => { - const coverId = ctx.validateParam('cover_id') - .required('the "cover_id" parameter is required') - .notEmpty() - .isString('the "cover_id" parameter should be String type') - .val() - - const data = await netease.neteaseMusic.picture(coverId) - - ctx.success(data) + const coverId = ctx.validateParam('cover_id').required('缺少封面ID').notEmpty().val() + const data = await netease.neteaseMusic.picture(coverId) + ctx.success(data) } diff --git a/server/controller/option.js b/server/controller/option.js index 1640fdf..bf66bfb 100644 --- a/server/controller/option.js +++ b/server/controller/option.js @@ -7,24 +7,21 @@ 'use strict' const { optionProxy } = require('../proxy') -const { updateMusicCache } = require('./music') -const { getDebug, proxy } = require('../util') const { modelUpdate } = require('../service') -const debug = getDebug('Option') // 站点参数数据 exports.data = async (ctx, next) => { - const data = await optionProxy.findOne().exec() - data - ? ctx.success(data, '站点参数获取成功') - : ctx.fail('站点参数获取失败') + const data = await optionProxy.findOne().exec() + data + ? ctx.success(data, '站点参数获取成功') + : ctx.fail('站点参数获取失败') } // 站点参数更新 exports.update = async (ctx, next) => { - const option = ctx.request.body - const data = await modelUpdate.updateOption(option) - data - ? ctx.success(data, '站点参数更新成功') - : ctx.fail('站点参数更新失败') + const option = ctx.request.body + const data = await modelUpdate.updateOption(option) + data + ? ctx.success(data, '站点参数更新成功') + : ctx.fail('站点参数更新失败') } diff --git a/server/controller/tag.js b/server/controller/tag.js index 27c1d46..8b24e25 100644 --- a/server/controller/tag.js +++ b/server/controller/tag.js @@ -10,114 +10,114 @@ const { tagProxy, articleProxy } = require('../proxy') // 标签列表 exports.list = async (ctx, next) => { - const keyword = ctx.validateQuery('keyword').optional().toString().val() - - const query = {} - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { name: keywordReg } - ] - } - - const data = await tagProxy.find(query).sort('-createdAt') - - if (data) { - for (let i = 0; i < data.length; i++) { - if (typeof data[i].toObject === 'function') { - data[i] = data[i].toObject() - } - const articles = await articleProxy.find({ tag: data[i]._id }).exec().catch(err => { - ctx.log.error(err.message) - return [] - }) - data[i].count = articles.length - } - ctx.success(data, '标签列表获取成功') - } else { - ctx.fail('标签列表获取失败') - } + const keyword = ctx.validateQuery('keyword').optional().toString().val() + + const query = {} + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { name: keywordReg } + ] + } + + const data = await tagProxy.find(query).sort('-createdAt') + + if (data) { + for (let i = 0; i < data.length; i++) { + if (typeof data[i].toObject === 'function') { + data[i] = data[i].toObject() + } + const articles = await articleProxy.find({ tag: data[i]._id }).exec().catch(err => { + ctx.log.error(err.message) + return [] + }) + data[i].count = articles.length + } + ctx.success(data, '标签列表获取成功') + } else { + ctx.fail('标签列表获取失败') + } } // 标签详情 exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() - - let data = await tagProxy.getById(id).exec() - - if (data) { - data = data.toObject() - const articles = await articleProxy.find({ tag: id }) - .select('-tag') - .exec() - .catch(err => { - ctx.log.error(err.message) - return [] - }) - data.articles = articles - data.articles_count = articles.length - ctx.success(data, '标签详情获取成功') - } else { - ctx.fail('标签详情获取失败') - } + const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() + + let data = await tagProxy.getById(id).exec() + + if (data) { + data = data.toObject() + const articles = await articleProxy.find({ tag: id }) + .select('-tag') + .exec() + .catch(err => { + ctx.log.error(err.message) + return [] + }) + data.articles = articles + data.articles_count = articles.length + ctx.success(data, '标签详情获取成功') + } else { + ctx.fail('标签详情获取失败') + } } // 标签创建 exports.create = async (ctx, next) => { - const name = ctx.validateBody('name').required('缺少标签名称').notEmpty().val() - const description = ctx.validateBody('description').optional().val() - const ext = ctx.validateBody('extends').optional().toArray().val() - - const { length } = await tagProxy.find({ name }).exec().catch(err => { - ctx.log.error(err.message) - return [] - }) - - if (!length) { - const data = await tagProxy.newAndSave({ - name, - description, - extends: ext - }) - - data && data.length - ? ctx.success(data, '标签创建成功') - : ctx.fail('标签创建失败') - } else { - ctx.fail(`【${name}】标签已经存在`) - } + const name = ctx.validateBody('name').required('缺少标签名称').notEmpty().val() + const description = ctx.validateBody('description').optional().val() + const ext = ctx.validateBody('extends').optional().toArray().val() + + const { length } = await tagProxy.find({ name }).exec().catch(err => { + ctx.log.error(err.message) + return [] + }) + + if (!length) { + const data = await tagProxy.newAndSave({ + name, + description, + extends: ext + }) + + data && data.length + ? ctx.success(data, '标签创建成功') + : ctx.fail('标签创建失败') + } else { + ctx.fail(`【${name}】标签已经存在`) + } } // 标签更新 exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() - const name = ctx.validateBody('name').optional().val() - const description = ctx.validateBody('description').optional().val() - const tag = {} + const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() + const name = ctx.validateBody('name').optional().val() + const description = ctx.validateBody('description').optional().val() + const tag = {} - name && (tag.name = name) - description && (tag.description = description) + name && (tag.name = name) + description && (tag.description = description) - const data = await tagProxy.updateById(id, tag).exec() + const data = await tagProxy.updateById(id, tag).exec() - data - ? ctx.success(data, '标签更新成功') - : ctx.fail('标签更新失败') + data + ? ctx.success(data, '标签更新成功') + : ctx.fail('标签更新失败') } // 标签删除 exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() - const articles = await tagProxy.find({ tag: id }).exec() - - if (articles && articles.length) { - // 标签下面有文章,不能删除 - ctx.fail('该标签下有文章,不能删除') - } else { - const data = await tagProxy.delById(id).exec() - data && data.result && data.result.ok - ? ctx.success(null, '标签删除成功') - : ctx.fail('标签删除失败') - } + const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() + const articles = await tagProxy.find({ tag: id }).exec() + + if (articles && articles.length) { + // 标签下面有文章,不能删除 + ctx.fail('该标签下有文章,不能删除') + } else { + const data = await tagProxy.delById(id).exec() + data && data.result && data.result.ok + ? ctx.success(null, '标签删除成功') + : ctx.fail('标签删除失败') + } } diff --git a/server/controller/user.js b/server/controller/user.js index 624680d..d4a9026 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -8,151 +8,148 @@ const config = require('../config') const { userProxy, commentProxy } = require('../proxy') -const { UserModel, CommentModel } = require('../model') -const { bhash, bcompare, getDebug, proxy } = require('../util') -const { getGithubUsersInfo } = require('../service') -const debug = getDebug('User') +const { bhash, bcompare } = require('../util') // 用户列表 exports.list = async (ctx, next) => { - let select = '-password' + let select = '-password' - if (!ctx._isAuthenticated) { - select += ' -createdAt -updatedAt -role' - } + if (!ctx._isAuthenticated) { + select += ' -createdAt -updatedAt -role' + } - const data = await userProxy.find() - .sort('-createdAt') - .select(select) + const data = await userProxy.find() + .sort('-createdAt') + .select(select) - data - ? ctx.success(data, '用户列表获取成功') - : ctx.fail('用户列表获取失败') + data + ? ctx.success(data, '用户列表获取成功') + : ctx.fail('用户列表获取失败') } // 用户详情 exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少用户ID').toString().isObjectId().val() - let select = '-password' - - if (!ctx._isAuthenticated) { - select += ' -createdAt -updatedAt -github' - } - - const data = await userProxy.getById(id) - .select(select) - .exec() - - data - ? ctx.success(data, '用户详情获取成功') - : ctx.fail('用户详情获取失败') + const id = ctx.validateParam('id').required('缺少用户ID').toString().isObjectId().val() + let select = '-password' + + if (!ctx._isAuthenticated) { + select += ' -createdAt -updatedAt -github' + } + + const data = await userProxy.getById(id) + .select(select) + .exec() + + data + ? ctx.success(data, '用户详情获取成功') + : ctx.fail('用户详情获取失败') } // 用户更新,只能更新自己 exports.updateMe = async (ctx, next) => { - const name = ctx.validateBody('name').optional().val() - const email = ctx.validateBody('email').optional().isEmail('Email格式错误').val() - const site = ctx.validateBody('site').optional().val() - const description = ctx.validateBody('description').optional().val() - const avatar = ctx.validateBody('avatar').optional().val() - const slogan = ctx.validateBody('slogan').optional().val() - const company = ctx.validateBody('company').optional().val() - const location = ctx.validateBody('location').optional().val() - const user = {} - - name && (user.name = name) - slogan && (user.slogan = slogan) - company && (user.company = company) - location && (user.location = location) - site && (user.site = site) - description && (user.description = description) - avatar && (user.avatar = avatar) - email && (user.email = email) - - const data = await userProxy.updateById(ctx._user._id, user).exec() - data - ? ctx.success(data, '用户更新成功') - : ctx.fail('用户更新失败') + const name = ctx.validateBody('name').optional().val() + const email = ctx.validateBody('email').optional().isEmail('Email格式错误').val() + const site = ctx.validateBody('site').optional().val() + const description = ctx.validateBody('description').optional().val() + const avatar = ctx.validateBody('avatar').optional().val() + const slogan = ctx.validateBody('slogan').optional().val() + const company = ctx.validateBody('company').optional().val() + const location = ctx.validateBody('location').optional().val() + const user = {} + + name && (user.name = name) + slogan && (user.slogan = slogan) + company && (user.company = company) + location && (user.location = location) + site && (user.site = site) + description && (user.description = description) + avatar && (user.avatar = avatar) + email && (user.email = email) + + const data = await userProxy.updateById(ctx._user._id, user).exec() + data + ? ctx.success(data, '用户更新成功') + : ctx.fail('用户更新失败') } // 更新密码 exports.password = async (ctx, next) => { - const password = ctx.validateBody('password').required('缺少新密码').notEmpty().val() - const oldPassword = ctx.validateBody('old_password').required('缺少原密码').notEmpty().val() - const vertifyPassword = bcompare(oldPassword, ctx._user.password) - if (!vertifyPassword) { - return ctx.fail('原密码错误') - } - - const data = await userProxy.updateById(ctx._user._id, { - password: bhash(password) - }).exec() - data - ? ctx.success(data, '密码更新成功') - : ctx.fail('密码更新失败') + const password = ctx.validateBody('password').required('缺少新密码').notEmpty().val() + const oldPassword = ctx.validateBody('old_password').required('缺少原密码').notEmpty().val() + const vertifyPassword = bcompare(oldPassword, ctx._user.password) + if (!vertifyPassword) { + return ctx.fail('原密码错误') + } + + const data = await userProxy.updateById(ctx._user._id, { + password: bhash(password) + }).exec() + data + ? ctx.success(data, '密码更新成功') + : ctx.fail('密码更新失败') } // 用户禁言/解禁 exports.mute = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少用户ID').toString().isObjectId().val() - const mute = ctx.validateBody('mute').defaultTo(true).toBoolean().val() - const data = await userProxy.updateById(id, { mute }).exec() - const msg = mute ? '用户禁言' : '用户解禁' - data - ? ctx.success(null, `${msg}成功`) - : ctx.fail(`${msg}失败`) + const id = ctx.validateParam('id').required('缺少用户ID').toString().isObjectId().val() + const mute = ctx.validateBody('mute').defaultTo(true).toBoolean().val() + const data = await userProxy.updateById(id, { mute }).exec() + const msg = mute ? '用户禁言' : '用户解禁' + data + ? ctx.success(null, `${msg}成功`) + : ctx.fail(`${msg}失败`) } // 博主信息获取 exports.blogger = async (ctx, next) => { - const data = await userProxy - .findOne({ 'github.login': config.author, role: 0 }) - .select('-password -role -createdAt -updatedAt -github -mute') - .exec() - - data - ? ctx.success(data, '博主详情获取成功') - : ctx.fail('博主详情获取失败') + const data = await userProxy + .findOne({ 'github.login': config.author, role: 0 }) + .select('-password -role -createdAt -updatedAt -github -mute') + .exec() + + data + ? ctx.success(data, '博主详情获取成功') + : ctx.fail('博主详情获取失败') } // 站内留言墙的用户,只限站内留言 exports.guests = async (ctx, next) => { - // OPTIMIZE: $lookup尝试失败,只能循环查询用户了 - let data = await commentProxy.aggregate([ - { - $match: { - spam: false, // 非垃圾留言 - state: 1, // 审核通过 - type: 1 // 站内留言 - } - }, - { - $sort: { - createdAt: -1 - } - }, - { - $group: { - _id: '$author' - } - } - ]).exec() - - let list = await Promise.all((data || []).map((item) => { - return userProxy.findOne({ - _id: item._id, - $nor: [ - { - role: config.constant.roleMap.ADMIN - }, { - 'github.login': config.author - } - ] - }).select('name site avatar').exec() - })) - list = list.filter(item => !!item) - ctx.success({ - list, - total: list.length - }, '站内留言用户列表获取成功') + // OPTIMIZE: $lookup尝试失败,只能循环查询用户了 + let data = await commentProxy.aggregate([ + { + $match: { + spam: false, // 非垃圾留言 + state: 1, // 审核通过 + type: 1 // 站内留言 + } + }, + { + $sort: { + createdAt: -1 + } + }, + { + $group: { + _id: '$author' + } + } + ]).exec() + + let list = await Promise.all((data || []).map(item => { + return userProxy.findOne({ + _id: item._id, + $nor: [ + { + role: config.constant.roleMap.ADMIN + }, { + 'github.login': config.author + } + ] + }).select('name site avatar').exec() + })) + list = list.filter(item => !!item) + ctx.success({ + list, + total: list.length + }, '站内留言用户列表获取成功') } diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index a108a95..06226b7 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -8,113 +8,113 @@ const compose = require('koa-compose') const jwt = require('jsonwebtoken') -const passport = require('koa-passport') +// const passport = require('koa-passport') const config = require('../config') const { userProxy } = require('../proxy') const debug = require('../util').getDebug('Auth') -const isProd = process.env.NODE_ENV === 'production' +// const isProd = process.env.NODE_ENV === 'production' const redirectReg = /auth\/github\/login(.*?)/ // 验证本地登录token function verifyToken () { - return async (ctx, next) => { - ctx.session._verify = false - const token = ctx.cookies.get(config.auth.session.key) + return async (ctx, next) => { + ctx.session._verify = false + const token = ctx.cookies.get(config.auth.session.key) - if (token) { - let decodedToken = null - try { - decodedToken = await jwt.verify(token, config.auth.secrets) - } catch (err) { - debug.error('本地登录Token校验出错,错误:', err.message) - if (redirectReg.test(ctx.originalUrl)) { - return ctx.redirect(ctx.query.redirectUrl || config.site) - } - return ctx.fail(401) - } + if (token) { + let decodedToken = null + try { + decodedToken = await jwt.verify(token, config.auth.secrets) + } catch (err) { + debug.error('本地登录Token校验出错,错误:', err.message) + if (redirectReg.test(ctx.originalUrl)) { + return ctx.redirect(ctx.query.redirectUrl || config.site) + } + return ctx.fail(401) + } - if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { - // 已校验权限 - ctx.session._verify = true - ctx.session._token = token - debug.success('本地登录Token校验成功') - } - } - await next() - } + if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { + // 已校验权限 + ctx.session._verify = true + ctx.session._token = token + debug.success('本地登录Token校验成功') + } + } + await next() + } } // 验证第三方登录token // function vertifySnsToken (name = '') { -// return async (ctx, next) => { -// if (ctx.session._snsVerify) { -// await next() -// } -// ctx.session._snsVerify = false -// if (config.sns[name]) { -// const token = ctx.cookies.get(config.sns[name].key, { signed: false }) +// return async (ctx, next) => { +// if (ctx.session._snsVerify) { +// await next() +// } +// ctx.session._snsVerify = false +// if (config.sns[name]) { +// const token = ctx.cookies.get(config.sns[name].key, { signed: false }) -// if (token) { -// ctx.session._snsVerify = true -// ctx.session._snsToken = token -// ctx.session._snsName = name -// debug.success('【%s】第三方登录Token校验成功', name) -// } -// } -// await next() -// } +// if (token) { +// ctx.session._snsVerify = true +// ctx.session._snsToken = token +// ctx.session._snsName = name +// debug.success('【%s】第三方登录Token校验成功', name) +// } +// } +// await next() +// } // } // 本地登录验证 exports.isAuthenticated = () => { - return compose([ - verifyToken(), - async (ctx, next) => { - if (!ctx.session._verify) { - return ctx.fail(401) - } + return compose([ + verifyToken(), + async (ctx, next) => { + if (!ctx.session._verify) { + return ctx.fail(401) + } - const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) - const user = await userProxy.getById(userId).exec().catch(err => { - debug.error('token验证时用户查找失败, 错误:', err.message) - ctx.log.error(err.message) - return null - }) - if (!user) { - return ctx.fail(401, '用户不存在') - } - ctx._user = user.toObject() - ctx._isAuthenticated = true - await next() - } - ]) + const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) + const user = await userProxy.getById(userId).exec().catch(err => { + debug.error('token验证时用户查找失败, 错误:', err.message) + ctx.log.error(err.message) + return null + }) + if (!user) { + return ctx.fail(401, '用户不存在') + } + ctx._user = user.toObject() + ctx._isAuthenticated = true + await next() + } + ]) } // 第三方登录验证 // exports.isSnsAuthenticated = () => { -// return compose( -// Object.keys(config.sns).map(name => { -// return vertifySnsToken(name) -// }).concat([ -// async (ctx, next) => { -// if (!ctx.session._snsVerify) { -// return ctx.fail(401) -// } +// return compose( +// Object.keys(config.sns).map(name => { +// return vertifySnsToken(name) +// }).concat([ +// async (ctx, next) => { +// if (!ctx.session._snsVerify) { +// return ctx.fail(401) +// } -// const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) -// const user = await UserModel.findById(userId).exec().catch(err => { -// debug.error('用户查找失败, 错误:', err.message) -// ctx.log.error(err.message) -// return null -// }) -// if (!user) { -// return ctx.fail(401, '用户不存在') -// } -// ctx._user = user.toObject() -// ctx._isSnsAuthenticated = true -// await next() -// } -// ])) +// const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) +// const user = await UserModel.findById(userId).exec().catch(err => { +// debug.error('用户查找失败, 错误:', err.message) +// ctx.log.error(err.message) +// return null +// }) +// if (!user) { +// return ctx.fail(401, '用户不存在') +// } +// ctx._user = user.toObject() +// ctx._isSnsAuthenticated = true +// await next() +// } +// ])) // } // 单个第三方登录验证 @@ -138,7 +138,7 @@ exports.isAuthenticated = () => { // ]) // } -// // 单个第三方登录退出 +// 单个第三方登录退出 // exports.snsLogout = (name = '') => compose([ // vertifySnsToken(name), // async (ctx, next) => { diff --git a/server/middleware/error.js b/server/middleware/error.js index 486b75d..26ba1a6 100644 --- a/server/middleware/error.js +++ b/server/middleware/error.js @@ -7,27 +7,24 @@ 'use strict' module.exports = async (ctx, next) => { - try { - await next() - } catch (err) { - let code = err.status || 500 - - if (err.name === 'ValidationError') { - code = 10001 - } - - ctx.fail(code, err.message) - ctx.status = 200 - - if (code === 500) { - // TODO: 错误日志记录 - ctx.log.error( - { req: ctx.req, err }, - ' --> %s %s %d', - ctx.request.method, - ctx.request.originalUrl, - ctx.status - ) - } - } -} \ No newline at end of file + try { + await next() + } catch (err) { + let code = err.status || 500 + if (err.name === 'ValidationError') { + code = 10001 + } + ctx.fail(code, err.message) + ctx.status = 200 + if (code === 500) { + // TODO: 错误日志记录 + ctx.log.error( + { req: ctx.req, err }, + ' --> %s %s %d', + ctx.request.method, + ctx.request.originalUrl, + ctx.status + ) + } + } +} diff --git a/server/middleware/formidable.js b/server/middleware/formidable.js index ecc4d94..57795d3 100644 --- a/server/middleware/formidable.js +++ b/server/middleware/formidable.js @@ -9,31 +9,31 @@ const formidable = require('formidable') const middleware = (opts = {}) => async (ctx, next) => { - const res = await middleware.parse(opts, ctx).catch(err => { - ctx.log.error(err.message) - return null - }) - if (res) { - ctx.request.body = res.fields - ctx.request.files = res.files - } - await next() + const res = await middleware.parse(opts, ctx).catch(err => { + ctx.log.error(err.message) + return null + }) + if (res) { + ctx.request.body = res.fields + ctx.request.files = res.files + } + await next() } middleware.parse = (opts = {}, ctx) => { - return new Promise((resolve, reject) => { - const form = new formidable.IncomingForm() - for(const key in opts){ - form[key] = opts[key] - } - form.parse(ctx.request, (err, fields, files) => { - if (err) return reject(err) - resolve({ - fields, - files - }) - }) - }) + return new Promise((resolve, reject) => { + const form = new formidable.IncomingForm() + for (const key in opts) { + form[key] = opts[key] + } + form.parse(ctx.request, (err, fields, files) => { + if (err) return reject(err) + resolve({ + fields, + files + }) + }) + }) } module.exports = middleware diff --git a/server/middleware/header.js b/server/middleware/header.js index 1a13e83..bdd1a32 100644 --- a/server/middleware/header.js +++ b/server/middleware/header.js @@ -9,24 +9,24 @@ const config = require('../config') module.exports = async (ctx, next) => { - const { request, response } = ctx - const allowedOrigins = config.constant.allowedOrigins - const origin = request.get('origin') || '' - const allowed = request.query._DEV_ - || origin.includes('localhost') - || origin.includes('127.0.0.1') - || allowedOrigins.find(item => origin.includes(item)) - if (allowed) { - response.set('Access-Control-Allow-Origin', origin) - } - response.set("Access-Control-Allow-Headers", "Authorization, Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With") - response.set("Access-Control-Allow-Methods", "PUT,PATCH,POST,GET,DELETE,OPTIONS") - response.set("Access-Control-Allow-Credentials", true) - response.set("Content-Type", "application/json;charset=utf-8") - response.set("X-Powered-By", `${config.name}/${config.version}`) + const { request, response } = ctx + const allowedOrigins = config.constant.allowedOrigins + const origin = request.get('origin') || '' + const allowed = request.query._DEV_ || + origin.includes('localhost') || + origin.includes('127.0.0.1') || + allowedOrigins.find(item => origin.includes(item)) + if (allowed) { + response.set('Access-Control-Allow-Origin', origin) + } + response.set('Access-Control-Allow-Headers', 'Authorization, Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With') + response.set('Access-Control-Allow-Methods', 'PUT,PATCH,POST,GET,DELETE,OPTIONS') + response.set('Access-Control-Allow-Credentials', true) + response.set('Content-Type', 'application/json;charset=utf-8') + response.set('X-Powered-By', `${config.name}/${config.version}`) - if (request.method === 'OPTIONS') { - return ctx.success('ok') - } - await next() + if (request.method === 'OPTIONS') { + return ctx.success('ok') + } + await next() } diff --git a/server/middleware/response.js b/server/middleware/response.js index 1736744..c45cb08 100644 --- a/server/middleware/response.js +++ b/server/middleware/response.js @@ -12,30 +12,30 @@ const successMsg = config.constant.codeMap['200'] const failMsg = config.constant.codeMap['-1'] module.exports = async (ctx, next) => { - ctx.success = (data = null, message = successMsg) => { - ctx.status = 200 - ctx.body = { - code: 200, - success: true, - message, - data - } - } + ctx.success = (data = null, message = successMsg) => { + ctx.status = 200 + ctx.body = { + code: 200, + success: true, + message, + data + } + } - ctx.fail = (code = -1, message = '', data = null) => { - if (isType(code, 'String')) { - data = message || null - message = code - code = -1 - } - ctx.status = 200 - ctx.body = { - code, - success: false, - message: message || config.constant.codeMap[code] || failMsg, - data - } - } + ctx.fail = (code = -1, message = '', data = null) => { + if (isType(code, 'String')) { + data = message || null + message = code + code = -1 + } + ctx.status = 200 + ctx.body = { + code, + success: false, + message: message || config.constant.codeMap[code] || failMsg, + data + } + } - await next() + await next() } diff --git a/server/model/index.js b/server/model/index.js index 3bef373..d9842e8 100644 --- a/server/model/index.js +++ b/server/model/index.js @@ -12,28 +12,28 @@ const { firstUpperCase } = require('../util') const models = {} Object.keys(schemas).forEach(key => { - const schema = getSchema(schemas[key]) - if (schema) { - models[`${firstUpperCase(key)}Model`] = mongoose.model(firstUpperCase(key), schema) - } + const schema = getSchema(schemas[key]) + if (schema) { + models[`${firstUpperCase(key)}Model`] = mongoose.model(firstUpperCase(key), schema) + } }) // 构建schema function getSchema (schema) { - if (!schema) { - return null - } - schema.set('versionKey', false) - schema.set('toObject', { getters: true }) - schema.set('toJSON', { getters: true, virtuals: false }) - schema.pre('findOneAndUpdate', updateHook) - return schema + if (!schema) { + return null + } + schema.set('versionKey', false) + schema.set('toObject', { getters: true }) + schema.set('toJSON', { getters: true, virtuals: false }) + schema.pre('findOneAndUpdate', updateHook) + return schema } // 更新updatedAt function updateHook (next) { - this.findOneAndUpdate({}, { updatedAt: Date.now() }) - next() + this.findOneAndUpdate({}, { updatedAt: Date.now() }) + next() } module.exports = models diff --git a/server/model/schema/article.js b/server/model/schema/article.js index 027169d..d057890 100644 --- a/server/model/schema/article.js +++ b/server/model/schema/article.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc Article Model * @author Jooger * @date 25 Sep 2017 */ @@ -10,38 +10,38 @@ const mongoose = require('mongoose') const mongoosePaginate = require('mongoose-paginate') const articleSchema = new mongoose.Schema({ - // 文章标题 - title: { type: String, required: true }, - // 文章关键字(FOR SEO) - keywords: [{ type: String }], - // 文章摘要 (FOR SEO) - description: { type: String, default: '' }, - // 文章原始markdown内容 - content: { type: String, required: true, validate: /\S+/ }, - // markdown渲染后的htmln内容 - renderedContent: { type: String, required: true, validate: /\S+/ }, - // 分类 - category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' }, - // 标签 - tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], - // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) - thumb: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, - // 文章状态 ( 0 草稿 | 1 已发布 ) - state: { type: Number, default: 0 }, - // 永久链接 - permalink: { type: String, validate: /\S+/ }, - // 创建日期 - createdAt: { type: Date, default: Date.now }, - // 更新日期 - updatedAt: { type: Date, default: Date.now }, - // 发布日期 - publishedAt: { type: Date, default: Date.now }, - // 文章元数据 (浏览量, 喜欢数, 评论数) - meta: { - pvs: { type: Number, default: 0 }, - ups: { type: Number, default: 0 }, - comments: { type: Number, default: 0 } - } + // 文章标题 + title: { type: String, required: true }, + // 文章关键字(FOR SEO) + keywords: [{ type: String }], + // 文章摘要 (FOR SEO) + description: { type: String, default: '' }, + // 文章原始markdown内容 + content: { type: String, required: true, validate: /\S+/ }, + // markdown渲染后的htmln内容 + renderedContent: { type: String, required: true, validate: /\S+/ }, + // 分类 + category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' }, + // 标签 + tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], + // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) + thumb: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, + // 文章状态 ( 0 草稿 | 1 已发布 ) + state: { type: Number, default: 0 }, + // 永久链接 + permalink: { type: String, validate: /\S+/ }, + // 创建日期 + createdAt: { type: Date, default: Date.now }, + // 更新日期 + updatedAt: { type: Date, default: Date.now }, + // 发布日期 + publishedAt: { type: Date, default: Date.now }, + // 文章元数据 (浏览量, 喜欢数, 评论数) + meta: { + pvs: { type: Number, default: 0 }, + ups: { type: Number, default: 0 }, + comments: { type: Number, default: 0 } + } }) articleSchema.plugin(mongoosePaginate) diff --git a/server/model/schema/category.js b/server/model/schema/category.js index 774883f..8ced579 100644 --- a/server/model/schema/category.js +++ b/server/model/schema/category.js @@ -9,16 +9,16 @@ const mongoose = require('mongoose') const categorySchema = new mongoose.Schema({ - name: { type: String, required: true }, - description: { type: String, default: '' }, - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, - // 排序 首页分类展示顺序 - list: { type: Number, default: 1 }, - extends: [{ - key: { type: String, validate: /\S+/ }, - value: { type: String, validate: /\S+/ } - }] + name: { type: String, required: true }, + description: { type: String, default: '' }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, + // 排序 首页分类展示顺序 + list: { type: Number, default: 1 }, + extends: [{ + key: { type: String, validate: /\S+/ }, + value: { type: String, validate: /\S+/ } + }] }) module.exports = categorySchema diff --git a/server/model/schema/comment.js b/server/model/schema/comment.js index cb26280..22bb0fc 100644 --- a/server/model/schema/comment.js +++ b/server/model/schema/comment.js @@ -1,5 +1,5 @@ /** - * @desc + * @desc Comment Model * @author Jooger * @date 25 Sep 2017 */ @@ -10,28 +10,28 @@ const mongoose = require('mongoose') const mongoosePaginate = require('mongoose-paginate') const commentSchema = new mongoose.Schema({ - // 评论通用项 - createdAt: { type: Date, default: Date.now }, // 创建时间 - updatedAt: { type: Date, default: Date.now }, // 修改时间 - content: { type: String, required: true, validate: /\S+/ }, // 评论内容 - renderedContent: { type: String, required: true, validate: /\S+/ }, // marked渲染后的内容 - state: { type: Number, default: 1 }, // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 - spam: { type: Boolean, default: false }, // Akismet判定是否是垃圾评论,方便后台check - author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, - ups: { type: Number, default: 0 }, // 点赞数 - sticky: { type: Number, default: 0 }, // 是否置顶 0 否 | 1 是 - type: { type: Number, default: 0 }, // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) - meta: { - ip: String, // 用户IP - location: Object, // IP所在地 - ua: { type: String, validate: /\S+/ }, // user agent - referer: { type: String, default: '' } - } , - // type为0时此项存在 - article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, - // 子评论具备项 - parent: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, // 父评论 parent和forward二者必须同时存在 - forward: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' } // 前一条评论ID,可以是parent的id, 比如 B评论 是 A评论的回复,则B.forward._id = A._id,主要是为了查看评论对话时的评论树构建 + // 评论通用项 + createdAt: { type: Date, default: Date.now }, // 创建时间 + updatedAt: { type: Date, default: Date.now }, // 修改时间 + content: { type: String, required: true, validate: /\S+/ }, // 评论内容 + renderedContent: { type: String, required: true, validate: /\S+/ }, // marked渲染后的内容 + state: { type: Number, default: 1 }, // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 + spam: { type: Boolean, default: false }, // Akismet判定是否是垃圾评论,方便后台check + author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + ups: { type: Number, default: 0 }, // 点赞数 + sticky: { type: Number, default: 0 }, // 是否置顶 0 否 | 1 是 + type: { type: Number, default: 0 }, // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) + meta: { + ip: String, // 用户IP + location: Object, // IP所在地 + ua: { type: String, validate: /\S+/ }, // user agent + referer: { type: String, default: '' } + }, + // type为0时此项存在 + article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, + // 子评论具备项 + parent: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, // 父评论 parent和forward二者必须同时存在 + forward: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' } // 前一条评论ID,可以是parent的id, 比如 B评论 是 A评论的回复,则B.forward._id = A._id,主要是为了查看评论对话时的评论树构建 }) commentSchema.plugin(mongoosePaginate) diff --git a/server/model/schema/log.js b/server/model/schema/log.js index d56c16b..b611717 100644 --- a/server/model/schema/log.js +++ b/server/model/schema/log.js @@ -10,7 +10,7 @@ const mongoose = require('mongoose') const mongoosePaginate = require('mongoose-paginate') const logSchema = new mongoose.Schema({ - createdAt: { type: Date, default: Date.now } + createdAt: { type: Date, default: Date.now } }) logSchema.plugin(mongoosePaginate) diff --git a/server/model/schema/moment.js b/server/model/schema/moment.js index edc5cbb..8619221 100644 --- a/server/model/schema/moment.js +++ b/server/model/schema/moment.js @@ -10,13 +10,13 @@ const mongoose = require('mongoose') const mongoosePaginate = require('mongoose-paginate') const momentSchema = new mongoose.Schema({ - content: { type: String, required: true, validate: /\S+/ }, - location: { type: Object, required: true }, - state: { type: Number, default: 1 }, // 状态 0 未发布 | 1 发布 - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now } + content: { type: String, required: true, validate: /\S+/ }, + location: { type: Object, required: true }, + state: { type: Number, default: 1 }, // 状态 0 未发布 | 1 发布 + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now } }) momentSchema.plugin(mongoosePaginate) -module.exports = momentSchema \ No newline at end of file +module.exports = momentSchema diff --git a/server/model/schema/option.js b/server/model/schema/option.js index 57b972d..8703a22 100644 --- a/server/model/schema/option.js +++ b/server/model/schema/option.js @@ -9,39 +9,39 @@ const mongoose = require('mongoose') const optionSchema = new mongoose.Schema({ - title: { type: String, default: '' }, - subtitle: { type: String, default: '' }, - welcome: { type: String, default: '' }, - description: [{ type: String, default: '' }], - banners: [{ type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }], - errorBanner: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, - hobby: [{ - name: { type: String, required: true }, - icon: { type: String, required: true } - }], - experience: [{ - time: { type: String, required: true }, - title: { type: String, required: true }, - subtitle: { type: String, default: '' } - }], - skill: [{ - title: { type: String, required: true }, - level: { type: String, required: true }, - icon: { type: String, required: true } - }], - contact: [{ - title: { type: String, required: true }, - url: { type: String, required: true }, - icon: { type: String, required: true } - }], - links: [{ - name: { type: String, required: true }, - github: { type: String, default: '' }, - avatar: { type: String, default: '' }, - slogan: { type: String, default: '' }, - site: { type: String, required: true } - }], - musicId: { type: String, default: '' }, + title: { type: String, default: '' }, + subtitle: { type: String, default: '' }, + welcome: { type: String, default: '' }, + description: [{ type: String, default: '' }], + banners: [{ type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }], + errorBanner: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, + hobby: [{ + name: { type: String, required: true }, + icon: { type: String, required: true } + }], + experience: [{ + time: { type: String, required: true }, + title: { type: String, required: true }, + subtitle: { type: String, default: '' } + }], + skill: [{ + title: { type: String, required: true }, + level: { type: String, required: true }, + icon: { type: String, required: true } + }], + contact: [{ + title: { type: String, required: true }, + url: { type: String, required: true }, + icon: { type: String, required: true } + }], + links: [{ + name: { type: String, required: true }, + github: { type: String, default: '' }, + avatar: { type: String, default: '' }, + slogan: { type: String, default: '' }, + site: { type: String, required: true } + }], + musicId: { type: String, default: '' } }) module.exports = optionSchema diff --git a/server/model/schema/tag.js b/server/model/schema/tag.js index 2258493..85116ae 100644 --- a/server/model/schema/tag.js +++ b/server/model/schema/tag.js @@ -9,14 +9,14 @@ const mongoose = require('mongoose') const tagSchema = new mongoose.Schema({ - name: { type: String, required: true }, - description: { type: String, default: '' }, - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, - extends: [{ - key: { type: String, validate: /\S+/ }, - value: { type: String, validate: /\S+/ } - }] + name: { type: String, required: true }, + description: { type: String, default: '' }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, + extends: [{ + key: { type: String, validate: /\S+/ }, + value: { type: String, validate: /\S+/ } + }] }) module.exports = tagSchema diff --git a/server/model/schema/user.js b/server/model/schema/user.js index fdb119a..c30705f 100644 --- a/server/model/schema/user.js +++ b/server/model/schema/user.js @@ -11,27 +11,27 @@ const config = require('../../config') const { isEmail, isSiteUrl } = require('../../util') const userSchema = new mongoose.Schema({ - name: { type: String, default: config.auth.defaultName, required: true }, - email: { type: String, required: true, validate: isEmail }, - avatar: { type: String, required: true }, - site: { type: String, validate: isSiteUrl }, - slogan: { type: String }, - description: { type: String, default: '' }, - // 角色 0 管理员 | 1 普通用户 | 2 github用户,不能更改 - role: { type: Number, default: 1 }, - // role = 0的时候才有该项 - password: { type: String }, - // 是否被禁言 - mute: { type: Boolean, default: false }, - company: { type: String, default: '' }, - location: { type: String, default: '' }, - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, - // github信息,不能手动更改 - github: { - id: { type: String, default: '' }, - login: { type: String, default: '' } - } + name: { type: String, default: config.auth.defaultName, required: true }, + email: { type: String, required: true, validate: isEmail }, + avatar: { type: String, required: true }, + site: { type: String, validate: isSiteUrl }, + slogan: { type: String }, + description: { type: String, default: '' }, + // 角色 0 管理员 | 1 普通用户 | 2 github用户,不能更改 + role: { type: Number, default: 1 }, + // role = 0的时候才有该项 + password: { type: String }, + // 是否被禁言 + mute: { type: Boolean, default: false }, + company: { type: String, default: '' }, + location: { type: String, default: '' }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, + // github信息,不能手动更改 + github: { + id: { type: String, default: '' }, + login: { type: String, default: '' } + } }) module.exports = userSchema diff --git a/server/plugins/akismet.js b/server/plugins/akismet.js index e2b3bde..05bd88f 100644 --- a/server/plugins/akismet.js +++ b/server/plugins/akismet.js @@ -8,7 +8,7 @@ const akismet = require('akismet-api') const config = require('../config') -const { isType, getDebug } = require('../util') +const { getDebug } = require('../util') const debug = getDebug('Akismet') let akismetClient = null @@ -21,119 +21,119 @@ let isValidKey = false * @param {String} [required] site Akismet site */ class AkismetClient { - constructor (key, site) { - this.key = key - this.site = site - this.initClient() - } + constructor (key, site) { + this.key = key + this.site = site + this.initClient() + } - initClient () { - this.client = akismet.client({ - key: this.key, - blog: this.site - }) - } + initClient () { + this.client = akismet.client({ + key: this.key, + blog: this.site + }) + } - async verifyKey () { - let valid = true - let error = '' - if (!isValidKey) { - await this.client.verifyKey().then(v => { - valid = v - if (v) { - isValidKey = true - } else { - error = '无效的Apikey' - this.client = null - } - }).catch(err => { - error = 'Apikey验证失败,错误:' + err.message - }) - } - return { valid, client: this, error } - } + async verifyKey () { + let valid = true + let error = '' + if (!isValidKey) { + await this.client.verifyKey().then(v => { + valid = v + if (v) { + isValidKey = true + } else { + error = '无效的Apikey' + this.client = null + } + }).catch(err => { + error = 'Apikey验证失败,错误:' + err.message + }) + } + return { valid, client: this, error } + } - // 检测是否是spam - checkSpam (opt = {}) { - debug.info('验证评论中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.checkSpam(opt, (err, spam) => { - if (err) { - debug.error('评论验证失败,将跳过Spam验证,错误:', err.message) - return reject(false) - } - if (spam) { - debug.warn('评论验证不通过,疑似垃圾评论') - resolve(true) - } else { - debug.success('评论验证通过') - resolve(false) - } - }) - } else { - debug.warn('Apikey未认证,将跳过Spam验证') - resolve(false) - } - }) - } + // 检测是否是spam + checkSpam (opt = {}) { + debug.info('验证评论中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.checkSpam(opt, (err, spam) => { + if (err) { + debug.error('评论验证失败,将跳过Spam验证,错误:', err.message) + return reject(false) + } + if (spam) { + debug.warn('评论验证不通过,疑似垃圾评论') + resolve(true) + } else { + debug.success('评论验证通过') + resolve(false) + } + }) + } else { + debug.warn('Apikey未认证,将跳过Spam验证') + resolve(false) + } + }) + } - // 提交被误检为spam的正常评论 - submitSpam (opt = {}) { - debug.info('误检Spam垃圾评论报告提交中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.submitSpam(opt, err => { - if (err) { - debug.error('误检Spam垃圾评论报告提交失败') - return reject(err) - } - debug.success('误检Spam垃圾评论报告提交成功') - resolve() - }) - } else { - debug.warn('Apikey未认证,误检Spam垃圾评论报告提交失败') - resolve() - } - }) - } + // 提交被误检为spam的正常评论 + submitSpam (opt = {}) { + debug.info('误检Spam垃圾评论报告提交中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.submitSpam(opt, err => { + if (err) { + debug.error('误检Spam垃圾评论报告提交失败') + return reject(err) + } + debug.success('误检Spam垃圾评论报告提交成功') + resolve() + }) + } else { + debug.warn('Apikey未认证,误检Spam垃圾评论报告提交失败') + resolve() + } + }) + } - // 提交被误检为正常评论的spam - submitHam (opt = {}) { - debug.info('误检正常评论报告提交中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.submitSpam(opt, err => { - if (err) { - debug.error('误检正常评论报告提交失败') - return reject(err) - } - debug.success('误检正常评论报告提交成功') - resolve() - }) - } else { - debug.warn('Apikey未认证,误检正常评论报告提交失败') - resolve() - } - }) - } + // 提交被误检为正常评论的spam + submitHam (opt = {}) { + debug.info('误检正常评论报告提交中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.submitSpam(opt, err => { + if (err) { + debug.error('误检正常评论报告提交失败') + return reject(err) + } + debug.success('误检正常评论报告提交成功') + resolve() + }) + } else { + debug.warn('Apikey未认证,误检正常评论报告提交失败') + resolve() + } + }) + } } /** * @desc 生成Akismet clients */ exports.start = async () => { - const akismetConfig = config.akismet - const { apiKey } = akismetConfig - const site = config.site - const { valid, client, error } = await new AkismetClient(apiKey, site).verifyKey() + const akismetConfig = config.akismet + const { apiKey } = akismetConfig + const site = config.site + const { valid, client, error } = await new AkismetClient(apiKey, site).verifyKey() - if (valid) { - debug.success('服务启动成功') - akismetClient = client - } else { - debug.error('服务启动失败', error ? `,${error}` : '') - } + if (valid) { + debug.success('服务启动成功') + akismetClient = client + } else { + debug.error('服务启动失败', error ? `,${error}` : '') + } } /** @@ -142,9 +142,9 @@ exports.start = async () => { * @return {AkismetClient} akismetClient Akismet client */ exports.getAkismetClient = () => { - if (!akismetClient) { - debug.warn('未找到客户端,将跳过spam验证') - return null - } - return akismetClient + if (!akismetClient) { + debug.warn('未找到客户端,将跳过spam验证') + return null + } + return akismetClient } diff --git a/server/plugins/crontab.js b/server/plugins/crontab.js index 106297a..a97d4f7 100644 --- a/server/plugins/crontab.js +++ b/server/plugins/crontab.js @@ -11,11 +11,13 @@ const { modelUpdate } = require('../service') const debug = getDebug('Crontab') exports.start = () => { - // 友链 每1小时更新一次 - modelUpdate.updateOption() - setInterval(modelUpdate.updateOption, 1000 * 60 * 60 * 1) + // 友链 每1小时更新一次 + modelUpdate.updateOption() + setInterval(modelUpdate.updateOption, 1000 * 60 * 60 * 1) - // 用户 每1天更新一次 - modelUpdate.updateGithubInfo() - setInterval(modelUpdate.updateGithubInfo, 1000 * 60 * 60 * 24) + // 用户 每1天更新一次 + modelUpdate.updateGithubInfo() + setInterval(modelUpdate.updateGithubInfo, 1000 * 60 * 60 * 24) + + debug.success('定时任务启动成功') } diff --git a/server/plugins/gc.js b/server/plugins/gc.js index 684c360..b3fc613 100644 --- a/server/plugins/gc.js +++ b/server/plugins/gc.js @@ -12,15 +12,15 @@ const debug = require('../util').getDebug('GC') const isProd = process.env.NODE_ENV === 'production' exports.start = (interval = 5000, delay = 5000) => { - if (isProd) { - setTimeout(() => { - gcStat().on('stats', stats => { - debug('回收完毕,用时 %s ms,共回收 %s KB堆内存', stats.pauseMS, stats.diff.totalHeapSize / 1000) - }) - gc.start(interval) - debug.success('服务启动成功') - }, delay) - } + if (isProd) { + setTimeout(() => { + gcStat().on('stats', stats => { + debug('回收完毕,用时 %s ms,共回收 %s KB堆内存', stats.pauseMS, stats.diff.totalHeapSize / 1000) + }) + gc.start(interval) + debug.success('服务启动成功') + }, delay) + } } exports.stop = () => gc.stop() diff --git a/server/plugins/mailer.js b/server/plugins/mailer.js index 3c72ee3..a7ac490 100644 --- a/server/plugins/mailer.js +++ b/server/plugins/mailer.js @@ -14,32 +14,32 @@ const isProd = process.env.NODE_ENV === 'production' let isVerify = false const transporter = isProd ? nodemailer.createTransport({ - service: '163', - secure: true, - auth: { - user: config.email, - pass: process.env['163Pass'] || '163邮箱密码' - } + service: '163', + secure: true, + auth: { + user: config.email, + pass: process.env['163Pass'] || '163邮箱密码' + } }) : null exports.start = async () => { - return new Promise((resolve, reject) => { - if (!transporter) { - return - } - transporter.verify((err, success) => { - if (err) { - isVerify = false - debug.error('服务启动失败,将在1分钟后重试,错误:', err.message) - reject(err) - setTimeout(exports.start, 60 * 1000) - } else { - isVerify = true - debug.success('服务启动成功') - resolve() - } - }) - }).catch(() => ({})) + return new Promise((resolve, reject) => { + if (!transporter) { + return + } + transporter.verify((err, success) => { + if (err) { + isVerify = false + debug.error('服务启动失败,将在1分钟后重试,错误:', err.message) + reject(err) + setTimeout(exports.start, 60 * 1000) + } else { + isVerify = true + debug.success('服务启动成功') + resolve() + } + }) + }) } /** @@ -48,18 +48,17 @@ exports.start = async () => { * @param {Boolean} toMe=false 是否是给自己发送邮件 */ exports.send = (opt = {}, toMe = false) => { - if (!isVerify) { - return debug.error('客户端未验证,拒绝发送邮件') - } - opt.from = `${config.author} <${config.email}>` - if (toMe) { - opt.to = config.email - } - transporter.sendMail(opt, (err, info) => { - if (err) { - return debug.error('邮件发送失败,错误:', err.message) - } - debug.success('邮件发送成功', info.messageId, info.response) - }) + if (!isVerify) { + return debug.error('客户端未验证,拒绝发送邮件') + } + opt.from = `${config.author} <${config.email}>` + if (toMe) { + opt.to = config.email + } + transporter.sendMail(opt, (err, info) => { + if (err) { + return debug.error('邮件发送失败,错误:', err.message) + } + debug.success('邮件发送成功', info.messageId, info.response) + }) } - diff --git a/server/plugins/mongo.js b/server/plugins/mongo.js index e637a6f..0b923f0 100644 --- a/server/plugins/mongo.js +++ b/server/plugins/mongo.js @@ -17,78 +17,77 @@ let isConnected = false mongoose.Promise = global.Promise exports.connect = () => { - mongoose.connect(config.mongo.uri, config.mongo.option).then(() => { - debug.success('连接成功') - isConnected = true - seed() - }, err => { - isConnected = false - return debug.error('连接失败,错误: ', config.mongo.uri, err.message) - }) + mongoose.connect(config.mongo.uri, config.mongo.option).then(() => { + debug.success('连接成功') + isConnected = true + seed() + }, err => { + isConnected = false + return debug.error('连接失败,错误: ', config.mongo.uri, err.message) + }) } exports.seed = seed function seed () { - if (isConnected) { - seedOption() - seedAdmin() - } + if (isConnected) { + seedOption() + seedAdmin() + } } // 参数初始化 async function seedOption () { - const option = await OptionModel.findOne().exec().catch(err => debug.error(err.message)) + const option = await OptionModel.findOne().exec().catch(err => debug.error(err.message)) - if (!option) { - await new OptionModel().save().catch(err => debug.error(err.message)) - } + if (!option) { + await new OptionModel().save().catch(err => debug.error(err.message)) + } } // 管理员初始化 async function seedAdmin () { - const admin = await UserModel.findOne({ - role: config.constant.roleMap.ADMIN, - 'github.login': config.author - }).exec() - .catch(err => debug.error('初始化管理员查询失败,错误:', err.message)) - if (!admin) { - createAdmin() - } + const admin = await UserModel.findOne({ + role: config.constant.roleMap.ADMIN, + 'github.login': config.author + }).exec() + .catch(err => debug.error('初始化管理员查询失败,错误:', err.message)) + if (!admin) { + createAdmin() + } } async function createAdmin () { - let data = await getGithubUsersInfo(config.auth.defaultName) - - if (!data || !data[0]) { - return fail('未找到Github用户数据') - } - data = data[0] - const result = await new UserModel({ - role: config.constant.roleMap.ADMIN, - name: data.name, - email: data.email, - password: bhash(config.auth.defaultPassword), - slogan: data.bio, - site: data.blog || data.url, - avatar: proxy(data.avatar_url), - company: data.company, - location: data.location, - github: { - id: data.id, - login: data.login - } - }) - .save() - .catch(err => fail(err.message)) + let data = await getGithubUsersInfo(config.auth.defaultName) + if (!data || !data[0]) { + return fail('未找到Github用户数据') + } + data = data[0] + const result = await new UserModel({ + role: config.constant.roleMap.ADMIN, + name: data.name, + email: data.email, + password: bhash(config.auth.defaultPassword), + slogan: data.bio, + site: data.blog || data.url, + avatar: proxy(data.avatar_url), + company: data.company, + location: data.location, + github: { + id: data.id, + login: data.login + } + }) + .save() + .catch(err => fail(err.message)) - if (!result || !result._id) { - fail('本地入库失败') - } else { - debug.success('初始化管理员成功') - } - - function fail (msg = '') { - debug.error('初始化管理员失败,错误:', msg) - } + if (!result || !result._id) { + fail('本地入库失败') + } else { + debug.success('初始化管理员成功') + } + + function fail (msg = '') { + debug.error('初始化管理员失败,错误:', msg) + } } diff --git a/server/plugins/redis.js b/server/plugins/redis.js index b0f46e2..0c09d93 100644 --- a/server/plugins/redis.js +++ b/server/plugins/redis.js @@ -15,61 +15,60 @@ let connected = false const cache = {} exports.connect = () => { - if (client) { - return debug('已连接') - } - client = redis.createClient(config.redis) - client.on('error', err => { - debug.error('连接失败, 错误: ', err.message) - connected = false - }) - client.on('connect', () => { - debug.success('连接成功') - connected = true - }) - client.on('reconnecting', () => debug('正在重连中...')) + if (client) { + return debug('已连接') + } + client = redis.createClient(config.redis) + client.on('error', err => { + debug.error('连接失败, 错误: ', err.message) + connected = false + }) + client.on('connect', () => { + debug.success('连接成功') + connected = true + }) + client.on('reconnecting', () => debug('正在重连中...')) } // 默认 1小时 过期 exports.set = (key = '', value = '', expired = 60 * 60) => new Promise((resolve, reject) => { - if (connected) { - if (!isType(value, 'String')) { - try { + if (connected) { + if (!isType(value, 'String')) { + try { value = JSON.stringify(value) } catch (err) { - debug.error('存储时,序列化失败, 错误:%s', err.message) + debug.error('存储时,序列化失败, 错误:%s', err.message) value = value.toString() } - } - client.set(key, value, 'EX', expired, (err, res) => { - if (err) { - debug.error('存储【 %s 】失败,错误:%s', key, err.message) - return reject(err) - } - resolve(res) - }) - } else { - cache[key] = value - resolve(value) - } + } + client.set(key, value, 'EX', expired, (err, res) => { + if (err) { + debug.error('存储【 %s 】失败,错误:%s', key, err.message) + return reject(err) + } + resolve(res) + }) + } else { + cache[key] = value + resolve(value) + } }) - exports.get = (key = '') => new Promise((resolve, reject) => { - if (connected) { - client.get(key, (err, res) => { - if (err) { - debug.error('读取【 %s 】失败,错误:%s', key, err.message) - return reject(err) - } - try { + if (connected) { + client.get(key, (err, res) => { + if (err) { + debug.error('读取【 %s 】失败,错误:%s', key, err.message) + return reject(err) + } + try { res = JSON.parse(res) } catch (err) { - debug.error('获取时,序列化失败, 错误:%s', err.message) + debug.error('获取时,序列化失败, 错误:%s', err.message) } - resolve(res) - }) - } else { - resolve(cache[key]) - } + resolve(res) + }) + } else { + resolve(cache[key]) + } }) diff --git a/server/plugins/validation.js b/server/plugins/validation.js index f6d01b1..a238dd2 100644 --- a/server/plugins/validation.js +++ b/server/plugins/validation.js @@ -11,35 +11,35 @@ const Validator = require('koa-bouncer').Validator const { isObjectId } = require('../util') Validator.addMethod('notEmpty', function (tip) { - this.isString(`${this.key}参数格式错误,期望格式:String`) - if (this.val().length === 0) { - this.throwError(tip || `${this.key}参数不能为空`) - } - return this + this.isString(`${this.key}参数格式错误,期望格式:String`) + if (this.val().length === 0) { + this.throwError(tip || `${this.key}参数不能为空`) + } + return this }) Validator.addMethod('isObjectId', function (tip) { - const val = this.val() - if (val !== undefined) { - this.toString() - if (!mongoose.Types.ObjectId.isValid(val)) { - this.throwError(tip || `${this.key}参数格式错误,期望格式:ObjectId`) - } - } - return this + const val = this.val() + if (val !== undefined) { + this.toString() + if (!mongoose.Types.ObjectId.isValid(val)) { + this.throwError(tip || `${this.key}参数格式错误,期望格式:ObjectId`) + } + } + return this }) Validator.addMethod('isObjectIdArray', function (tip) { - const val = this.val() - if (val !== undefined) { - this.isArray() - val.forEach(data => { - if (!isObjectId(data)) { - this.throwError(tip || `${this.key}参数格式错误,期望格式:[ObjectId]`) - } - }) - } - return this + const val = this.val() + if (val !== undefined) { + this.isArray() + val.forEach(data => { + if (!isObjectId(data)) { + this.throwError(tip || `${this.key}参数格式错误,期望格式:[ObjectId]`) + } + }) + } + return this }) module.exports = Validator diff --git a/server/proxy/article.js b/server/proxy/article.js index c9c0818..d681940 100644 --- a/server/proxy/article.js +++ b/server/proxy/article.js @@ -10,9 +10,9 @@ const BaseProxy = require('./base') const { ArticleModel } = require('../model') class ArticleProxy extends BaseProxy { - constructor () { - super(ArticleModel) - } + constructor () { + super(ArticleModel) + } } module.exports = new ArticleProxy() diff --git a/server/proxy/base.js b/server/proxy/base.js index ee37b15..11fa89d 100644 --- a/server/proxy/base.js +++ b/server/proxy/base.js @@ -7,71 +7,71 @@ 'use strict' module.exports = class BaseProxy { - constructor (Model) { - this.Model = Model - } + constructor (Model) { + this.Model = Model + } - newAndSave (docs) { - if (!Array.isArray(docs)) { - docs = [docs] - } - return this.Model.insertMany(docs) - } + newAndSave (docs) { + if (!Array.isArray(docs)) { + docs = [docs] + } + return this.Model.insertMany(docs) + } - paginate (query, opt = {}) { - return this.Model.paginate(query, opt) - } + paginate (query, opt = {}) { + return this.Model.paginate(query, opt) + } - getById (id) { - return this.Model.findById(id) - } + getById (id) { + return this.Model.findById(id) + } - find (query = {}, opt = {}) { - return this.Model.find(query, null, opt) - } + find (query = {}, opt = {}) { + return this.Model.find(query, null, opt) + } - findOne (query = {}, opt = {}) { - return this.Model.findOne(query, null, opt) - } + findOne (query = {}, opt = {}) { + return this.Model.findOne(query, null, opt) + } - updateById (id, doc, opt = {}) { - return this.Model.findByIdAndUpdate(id, doc, { - new: true, - ...opt - }) - } + updateById (id, doc, opt = {}) { + return this.Model.findByIdAndUpdate(id, doc, { + new: true, + ...opt + }) + } - updateOne (query = {}, doc = {}, opt = {}) { - return this.Model.findOneAndUpdate(query, doc, { - new: true, - ...opt - }) - } + updateOne (query = {}, doc = {}, opt = {}) { + return this.Model.findOneAndUpdate(query, doc, { + new: true, + ...opt + }) + } - updateMany (query = {}, doc = {}, opt = {}) { - return this.Model.update(query, doc, { - multi: true, - ...opt - }) - } + updateMany (query = {}, doc = {}, opt = {}) { + return this.Model.update(query, doc, { + multi: true, + ...opt + }) + } - del (query) { - return this.Model.remove(query) - } + del (query) { + return this.Model.remove(query) + } - delById (id) { - return this.del({ _id: id }) - } + delById (id) { + return this.del({ _id: id }) + } - delByIds (ids) { - return this.del({ - _id: { - $in: ids - } - }) - } + delByIds (ids) { + return this.del({ + _id: { + $in: ids + } + }) + } - aggregate (opt = {}) { - return this.Model.aggregate(opt) - } + aggregate (opt = {}) { + return this.Model.aggregate(opt) + } } diff --git a/server/proxy/category.js b/server/proxy/category.js index f5b60d3..bcae877 100644 --- a/server/proxy/category.js +++ b/server/proxy/category.js @@ -10,9 +10,9 @@ const BaseProxy = require('./base') const { CategoryModel } = require('../model') class CategoryProxy extends BaseProxy { - constructor () { - super(CategoryModel) - } + constructor () { + super(CategoryModel) + } } module.exports = new CategoryProxy() diff --git a/server/proxy/comment.js b/server/proxy/comment.js index 019a1d4..7e0f6c4 100644 --- a/server/proxy/comment.js +++ b/server/proxy/comment.js @@ -10,9 +10,9 @@ const BaseProxy = require('./base') const { CommentModel } = require('../model') class CommentProxy extends BaseProxy { - constructor () { - super(CommentModel) - } + constructor () { + super(CommentModel) + } } module.exports = new CommentProxy() diff --git a/server/proxy/index.js b/server/proxy/index.js index acdd63e..8b4680c 100644 --- a/server/proxy/index.js +++ b/server/proxy/index.js @@ -7,11 +7,11 @@ 'use strict' module.exports = { - articleProxy: require('./article'), - categoryProxy: require('./category'), - tagProxy: require('./tag'), - userProxy: require('./user'), - commentProxy: require('./comment'), - optionProxy: require('./option'), - momentProxy: require('./moment') + articleProxy: require('./article'), + categoryProxy: require('./category'), + tagProxy: require('./tag'), + userProxy: require('./user'), + commentProxy: require('./comment'), + optionProxy: require('./option'), + momentProxy: require('./moment') } diff --git a/server/proxy/moment.js b/server/proxy/moment.js index f14dfa7..32e318b 100644 --- a/server/proxy/moment.js +++ b/server/proxy/moment.js @@ -10,9 +10,9 @@ const BaseProxy = require('./base') const { MomentModel } = require('../model') class MomentProxy extends BaseProxy { - constructor () { - super(MomentModel) - } + constructor () { + super(MomentModel) + } } module.exports = new MomentProxy() diff --git a/server/proxy/option.js b/server/proxy/option.js index 7a37837..a3b367d 100644 --- a/server/proxy/option.js +++ b/server/proxy/option.js @@ -10,9 +10,9 @@ const BaseProxy = require('./base') const { OptionModel } = require('../model') class OptionProxy extends BaseProxy { - constructor () { - super(OptionModel) - } + constructor () { + super(OptionModel) + } } module.exports = new OptionProxy() diff --git a/server/proxy/tag.js b/server/proxy/tag.js index 690e331..948d67a 100644 --- a/server/proxy/tag.js +++ b/server/proxy/tag.js @@ -10,9 +10,9 @@ const BaseProxy = require('./base') const { TagModel } = require('../model') class TagProxy extends BaseProxy { - constructor () { - super(TagModel) - } + constructor () { + super(TagModel) + } } module.exports = new TagProxy() diff --git a/server/proxy/user.js b/server/proxy/user.js index 8189ad5..be4c54e 100644 --- a/server/proxy/user.js +++ b/server/proxy/user.js @@ -10,9 +10,9 @@ const BaseProxy = require('./base') const { UserModel } = require('../model') class UserProxy extends BaseProxy { - constructor () { - super(UserModel) - } + constructor () { + super(UserModel) + } } module.exports = new UserProxy() diff --git a/server/routes/backend.js b/server/routes/backend.js index 896cc0b..b5b81de 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -8,16 +8,16 @@ const router = require('koa-router')() const { - article, - category, - tag, - comment, - option, - user, - auth, - music, - statistics, - moment + article, + category, + tag, + comment, + option, + user, + auth, + music, + statistics, + moment } = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() @@ -25,7 +25,7 @@ const isAuthenticated = authenticate.isAuthenticated() // Article router.get('/articles', isAuthenticated, article.list) router.get('/articles/:id', isAuthenticated, article.item) -router.post('/articles',isAuthenticated, article.create) +router.post('/articles', isAuthenticated, article.create) router.patch('/articles/:id', isAuthenticated, article.update) router.delete('/articles/:id', isAuthenticated, article.delete) router.post('/articles/:id/like', isAuthenticated, article.like) diff --git a/server/routes/index.js b/server/routes/index.js index 5583d5f..7734c2a 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -13,27 +13,27 @@ const { header } = require('../middleware') const config = require('../config') module.exports = app => { - router.use('*', header) + router.use('*', header) - router.get('/', async (ctx, next) => { - ctx.log.info('Got a root request from %s for %s', ctx.request.ip, ctx.path) - ctx.body = { - name: config.name, - version: config.version, - author: config.author, - github: 'https://github.com/jo0ger', - site: config.site, - poweredBy: ['Koa2', 'MongoDB', 'Nginx'] - } - }) + router.get('/', async (ctx, next) => { + ctx.log.info('Got a root request from %s for %s', ctx.request.ip, ctx.path) + ctx.body = { + name: config.name, + version: config.version, + author: config.author, + github: 'https://github.com/jo0ger', + site: config.site, + poweredBy: ['Koa2', 'MongoDB', 'Nginx'] + } + }) - router.use('/backend', backend.routes(), backend.allowedMethods()) - router.use(frontend.routes(), frontend.allowedMethods()) + router.use('/backend', backend.routes(), backend.allowedMethods()) + router.use(frontend.routes(), frontend.allowedMethods()) - router.all('*', (ctx,next)=> { - ctx.fail(404, `${ctx.path} 不支持 ${ctx.method} 请求类型`) - ctx.status = 404 - }) + router.all('*', (ctx, next) => { + ctx.fail(404, `${ctx.path} 不支持 ${ctx.method} 请求类型`) + ctx.status = 404 + }) - app.use(router.routes(), router.allowedMethods()) + app.use(router.routes(), router.allowedMethods()) } diff --git a/server/service/github-passport.js b/server/service/github-passport.js index 6731be7..e80f2d1 100644 --- a/server/service/github-passport.js +++ b/server/service/github-passport.js @@ -14,76 +14,75 @@ const { randomString, getDebug, proxy } = require('../util') const debug = getDebug('Github:Auth') exports.init = (UserModel, config) => { - passport.use(new GithubStrategy({ - clientID, - clientSecret, - callbackURL, - passReqToCallback: true - }, async (req, accessToken, refreshToken, profile, done) => { - debug('Github权限验证开始...') - try { - const user = await UserModel.findOne({ - 'github.id': profile.id - }).catch(err => { - debug.error('本地用户查找失败, 错误:', err.message) - return null - }) + passport.use(new GithubStrategy({ + clientID, + clientSecret, + callbackURL, + passReqToCallback: true + }, async (req, accessToken, refreshToken, profile, done) => { + debug('Github权限验证开始...') + try { + const user = await UserModel.findOne({ + 'github.id': profile.id + }).catch(err => { + debug.error('本地用户查找失败, 错误:', err.message) + return null + }) - if (user) { - const userData = { - name: profile.displayName || profile.username, - avatar: proxy(profile._json.avatar_url), - slogan: profile._json.bio, - github: profile._json, - role: user.role - } - // userData.github.token = accessToken - const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData).exec().catch(err => { - debug.error('本地用户更新失败, 错误:', err.message) - }) || user + if (user) { + const userData = { + name: profile.displayName || profile.username, + avatar: proxy(profile._json.avatar_url), + slogan: profile._json.bio, + github: profile._json, + role: user.role + } + // userData.github.token = accessToken + const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData).exec().catch(err => { + debug.error('本地用户更新失败, 错误:', err.message) + }) || user - return end(null, { - ...updatedUser.toObject(), - token: accessToken - }) - } + return end(null, { + ...updatedUser.toObject(), + token: accessToken + }) + } - const newUser = { - name: profile.displayName || profile.username, - avatar: proxy(profile._json.avatar_url), - slogan: profile._json.bio, - github: profile._json, - role: 1 - } + const newUser = { + name: profile.displayName || profile.username, + avatar: proxy(profile._json.avatar_url), + slogan: profile._json.bio, + github: profile._json, + role: 1 + } - // newUser.github.token = accessToken + // newUser.github.token = accessToken - const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { - debug.error('本地用户查找失败, 错误:', err.message) - return true - }) + const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { + debug.error('本地用户查找失败, 错误:', err.message) + return true + }) - if (checkUser) { - newUser.name += '-' + randomString() - } + if (checkUser) { + newUser.name += '-' + randomString() + } - const data = await new UserModel(newUser).save().catch(err => { - debug.error('本地用户创建失败, 错误:', err.message) - }) + const data = await new UserModel(newUser).save().catch(err => { + debug.error('本地用户创建失败, 错误:', err.message) + }) - return end(null, { - ...data.toObject(), - token: access - }) - } catch (err) { - debug.error('Github权限验证失败,错误:', err) - return end(err) - } + return end(null, { + ...data.toObject(), + token: accessToken + }) + } catch (err) { + debug.error('Github权限验证失败,错误:', err) + return end(err) + } - function end (err, data) { - debug.success('Github权限验证成功') - done(err, data) - } - })) + function end (err, data) { + debug.success('Github权限验证成功') + done(err, data) + } + })) } - diff --git a/server/service/github-token.js b/server/service/github-token.js index a765231..c06aabe 100644 --- a/server/service/github-token.js +++ b/server/service/github-token.js @@ -12,24 +12,23 @@ const config = require('../config') const { clientID, clientSecret } = config.sns.github const debug = getDebug('Github:Token') -module.exports = async (code) => { - const data = await axios.post('https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token', { - client_id: clientID, - client_secret: clientSecret, - code - }, { - headers: { - Accept: 'application/json', - 'X-Requested-With': 'XMLHttpRequest' - } - }) - .catch(err => { - debug.error('Github Token获取失败,错误:', err.message) - return null - }) +module.exports = async code => { + const data = await axios.post('https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token', { + client_id: clientID, + client_secret: clientSecret, + code + }, { + headers: { + Accept: 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + } + }).catch(err => { + debug.error('Github Token获取失败,错误:', err.message) + return null + }) - if (data && data.data.access_token) { - return data.data - } - return null + if (data && data.data.access_token) { + return data.data + } + return null } diff --git a/server/service/github-userinfo.js b/server/service/github-userinfo.js index 514a089..d18f931 100644 --- a/server/service/github-userinfo.js +++ b/server/service/github-userinfo.js @@ -13,47 +13,46 @@ const { clientID, clientSecret } = config.sns.github const debug = getDebug('Github:User') exports.getGithubUsersInfo = (githubNames = '') => { - if (!githubNames) { - return null - } else if (typeof githubNames === 'string') { - githubNames = [githubNames] - } else if (!Array.isArray(githubNames)) { - return null - } + if (!githubNames) { + return null + } else if (typeof githubNames === 'string') { + githubNames = [githubNames] + } else if (!Array.isArray(githubNames)) { + return null + } - const task = githubNames.map(name => { - return axios.get(`https://api.github.com/users/${name}`, { - params: { - client_id: clientID, - client_secret: clientSecret - } - }, { - headers: { - Accept: 'application/json' - } - }).then(res => { - if (res && res.status === 200) { - debug.success('【 %s 】信息抓取成功', name) - return res.data - } - return null - }) - .catch(err => { - debug.error('【 %s 】信息抓取失败,错误:%s', name, err.message) - return null - }) - }) + const task = githubNames.map(name => { + return axios.get(`https://api.github.com/users/${name}`, { + params: { + client_id: clientID, + client_secret: clientSecret + } + }, { + headers: { + Accept: 'application/json' + } + }).then(res => { + if (res && res.status === 200) { + debug.success('【 %s 】信息抓取成功', name) + return res.data + } + return null + }).catch(err => { + debug.error('【 %s 】信息抓取失败,错误:%s', name, err.message) + return null + }) + }) - return Promise.all(task) + return Promise.all(task) } exports.getGithubAuthUserInfo = (access_token = '') => { - return axios.get('https://api.github.com/user', { - params: { access_token } - }).then(res => { - return res.data - }).catch(err => { - debug.error('获取用户信息失败,错误:', err.message) - return null - }) -} \ No newline at end of file + return axios.get('https://api.github.com/user', { + params: { access_token } + }).then(res => { + return res.data + }).catch(err => { + debug.error('获取用户信息失败,错误:', err.message) + return null + }) +} diff --git a/server/service/model-update.js b/server/service/model-update.js index 877de7c..e569a9a 100644 --- a/server/service/model-update.js +++ b/server/service/model-update.js @@ -17,173 +17,171 @@ const isProd = process.env.NODE_ENV === 'production' // update lock let updateOptionLock = false exports.updateOption = async (option = null) => { - if (updateOptionLock) { - debug.warn('站点参数更新中...') - return - } - updateOptionLock = true - if (!option) { - option = await optionProxy.findOne().exec().catch(err => { - debug.error('数据查找失败,错误:', err.message) - ctx.log.error(err.message) - return {} - }) - } + if (updateOptionLock) { + debug.warn('站点参数更新中...') + return + } + updateOptionLock = true + if (!option) { + option = await optionProxy.findOne().exec().catch(err => { + debug.error('数据查找失败,错误:', err.message) + return {} + }) + } - // 更新友链 - option.links = await generateLinks(option.links) + // 更新友链 + option.links = await generateLinks(option.links) - const data = await optionProxy.updateOne({}, option).exec().catch(err => { - debug.error('数据更新失败,错误:', err.message) - ctx.log.error(err.message) - return null - }) + const data = await optionProxy.updateOne({}, option).exec().catch(err => { + debug.error('数据更新失败,错误:', err.message) + return null + }) - if (data) { - debug.success('站点参数更新成功') - } - updateOptionLock = false - return data + if (data) { + debug.success('站点参数更新成功') + } + updateOptionLock = false + return data } // 更新github用户信息 exports.updateGithubInfo = async () => { - const users = await userProxy.find() - .exec() - .catch(err => { - debug.error('用户查找失败,错误:', err.message) - return [] - }) - const githubUsers = users.reduce((sum, user) => { - if (user.role === config.constant.roleMap.GITHUB_USER || (user.role === config.constant.roleMap.ADMIN && user.github.login)) { - sum.push(user) - } - return sum - }, []) - const updates = await getGithubUsersInfo(githubUsers.map(user => user.github.login)) - Promise.all( - updates.reduce((tasks, data, index) => { - const user = githubUsers[index] - const u = { - name: data.name, - email: data.email, - avatar: proxy(data.avatar_url), - site: data.blog || data.url, - slogan: data.bio, - company: data.company, - location: data.location, - github: { - id: data.id, - login: data.login - } - } - tasks.push( - userProxy.updateById(user._id, u).exec().catch(err => { - debug.error('Github用户信息更新失败,错误:', err.message) - return null - }) - ) - return tasks - }, []) - ).then(() => { - debug.success('所有Github用户信息更新成功') - }) + const users = await userProxy.find() + .exec() + .catch(err => { + debug.error('用户查找失败,错误:', err.message) + return [] + }) + const githubUsers = users.reduce((sum, user) => { + if (user.role === config.constant.roleMap.GITHUB_USER || (user.role === config.constant.roleMap.ADMIN && user.github.login)) { + sum.push(user) + } + return sum + }, []) + const updates = await getGithubUsersInfo(githubUsers.map(user => user.github.login)) + Promise.all( + updates.reduce((tasks, data, index) => { + const user = githubUsers[index] + const u = { + name: data.name, + email: data.email, + avatar: proxy(data.avatar_url), + site: data.blog || data.url, + slogan: data.bio, + company: data.company, + location: data.location, + github: { + id: data.id, + login: data.login + } + } + tasks.push( + userProxy.updateById(user._id, u).exec().catch(err => { + debug.error('Github用户信息更新失败,错误:', err.message) + return null + }) + ) + return tasks + }, []) + ).then(() => { + debug.success('所有Github用户信息更新成功') + }) } // 获取除了歌曲链接和歌词外其他信息 -exports.fetchSonglist = (playListId) => { - return netease.neteaseMusic._playlist(playListId).then(({ playlist }) => { - if (!playlist) { - return null - } - const tracks = playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { - return { - id, - name, - duration: dt || 0, - album: al && { - name: al.name, - cover: isProd ? (proxy(al.picUrl) || '') : al.picUrl, - tns: al.tns - } || {}, - artists: ar && ar.map(({ id, name }) => ({ id, name })) || [], - tns: tns || [] - } - }) - return { - id: playListId, - tracks, - name: playlist.name, - description: playlist.description, - tags: playlist.tags - } - }).catch(err => { - debug.error('歌单列表获取失败,错误:', err.message) - return null - }) +exports.fetchSonglist = playListId => { + return netease.neteaseMusic._playlist(playListId).then(({ playlist }) => { + if (!playlist) { + return null + } + const tracks = playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { + return { + id, + name, + duration: dt || 0, + album: al ? { + name: al.name, + cover: isProd ? (proxy(al.picUrl) || '') : al.picUrl, + tns: al.tns + } : {}, + artists: ar ? ar.map(({ id, name }) => ({ id, name })) : [], + tns: tns || [] + } + }) + return { + id: playListId, + tracks, + name: playlist.name, + description: playlist.description, + tags: playlist.tags + } + }).catch(err => { + debug.error('歌单列表获取失败,错误:', err.message) + return null + }) } // 更新song list cache let musicCacheLock = false exports.updateMusicCache = async function (playListId = '') { - const { redis } = require('../plugins') - if (musicCacheLock) { - debug.warn('缓存更新中...') - return redis.get(config.constant.redisCacheKey.music) || null - } - musicCacheLock = true - if (!playListId) { - const option = await optionProxy.findOne().exec().catch(err => { - debug.error('Option查找失败,错误:', err.message) - return null - }) + const { redis } = require('../plugins') + if (musicCacheLock) { + debug.warn('缓存更新中...') + return redis.get(config.constant.redisCacheKey.music) || null + } + musicCacheLock = true + if (!playListId) { + const option = await optionProxy.findOne().exec().catch(err => { + debug.error('Option查找失败,错误:', err.message) + return null + }) - if (!option || !option.musicId) { - debug.warn('歌单ID未配置') - musicCacheLock = false - return redis.get(config.constant.redisCacheKey.music) || null - } - playListId = option.musicId - } + if (!option || !option.musicId) { + debug.warn('歌单ID未配置') + musicCacheLock = false + return redis.get(config.constant.redisCacheKey.music) || null + } + playListId = option.musicId + } - const data = await exports.fetchSonglist(playListId) - if (!data) { - musicCacheLock = false - return redis.get(config.constant.redisCacheKey.music) || null - } - const set = { - id: playListId, - data - } - - // 设置10分钟过期 - redis.set(config.constant.redisCacheKey.music, set, 60 * 10).then(() => { - debug.success('缓存更新成功,歌单ID:', playListId) - }).catch(err => { - debug.error('缓存更新失败,歌单ID:%s,错误:%s', playListId, err.message) - }) + const data = await exports.fetchSonglist(playListId) + if (!data) { + musicCacheLock = false + return redis.get(config.constant.redisCacheKey.music) || null + } + const set = { + id: playListId, + data + } - musicCacheLock = false - return set + // 设置10分钟过期 + redis.set(config.constant.redisCacheKey.music, set, 60 * 10).then(() => { + debug.success('缓存更新成功,歌单ID:', playListId) + }).catch(err => { + debug.error('缓存更新失败,歌单ID:%s,错误:%s', playListId, err.message) + }) + + musicCacheLock = false + return set } // 更新友链 async function generateLinks (links = []) { - if (links && links.length) { - const githubNames = links.map(link => link.github) - const usersInfo = await getGithubUsersInfo(githubNames) + if (links && links.length) { + const githubNames = links.map(link => link.github) + const usersInfo = await getGithubUsersInfo(githubNames) - if (usersInfo) { - return links.map((link, index) => { - const userInfo = usersInfo[index] - if (userInfo) { - link.avatar = proxy(userInfo.avatar_url) - link.slogan = userInfo.bio - link.site = link.site || userInfo.blog || userInfo.url - } - return link - }) - } - } - return links + if (usersInfo) { + return links.map((link, index) => { + const userInfo = usersInfo[index] + if (userInfo) { + link.avatar = proxy(userInfo.avatar_url) + link.slogan = userInfo.bio + link.site = link.site || userInfo.blog || userInfo.url + } + return link + }) + } + } + return links } diff --git a/server/service/netease-music.js b/server/service/netease-music.js index ff09db3..5209738 100644 --- a/server/service/netease-music.js +++ b/server/service/netease-music.js @@ -13,89 +13,88 @@ const neteaseMusic = new NeteseMusic() const debug = getDebug('Netease') const neFetcher = axios.create({ - baseURL: 'http://music.163.com', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Accept': '*/*', - 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', - 'Connection': 'keep-alive', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Referer': 'http://music.163.com', - 'Host': 'music.163.com', - 'Cookie': 'appver=2.0.2;', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36', - } + baseURL: 'http://music.163.com', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': '*/*', + 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', + 'Connection': 'keep-alive', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': 'http://music.163.com', + 'Host': 'music.163.com', + 'Cookie': 'appver=2.0.2;', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36' + } }) const links = { - playlist: '/weapi/v3/playlist/detail', - song: '/weapi/v3/song/detail', - songUrl: '/weapi/song/enhance/player/url' + playlist: '/weapi/v3/playlist/detail', + song: '/weapi/v3/song/detail', + songUrl: '/weapi/song/enhance/player/url' } const fetchNE = function (type = '', id = '') { - return new Promise((resolve, reject) => { - if (!id) { - return reject(new Error('no id detect')) - } - let data = {} + return new Promise((resolve, reject) => { + if (!id) { + return reject(new Error('no id detect')) + } + let data = {} - switch (type) { - case 'playlist': - data = { - id: id, - offset: 0, - total: true, - limit: 1000, - n: 1000, - csrf_token: '' - } - break - case 'song': - data = { - c: JSON.stringify([{ id }]), - ids: `[${id}]`, - csrf_token: '' - } - break - case 'songUrl': - data = { - ids: [id], - br: 999000, - csrf_token: '' - } - break - case 'songlyric': - data = { - id, - os: 'linux', - lv: -1, - kv: -1, - tv: -1, - } - break - default: - return reject(new Error('no support type')) - break - } + switch (type) { + case 'playlist': + data = { + id: id, + offset: 0, + total: true, + limit: 1000, + n: 1000, + csrf_token: '' + } + break + case 'song': + data = { + c: JSON.stringify([{ id }]), + ids: `[${id}]`, + csrf_token: '' + } + break + case 'songUrl': + data = { + ids: [id], + br: 999000, + csrf_token: '' + } + break + case 'songlyric': + data = { + id, + os: 'linux', + lv: -1, + kv: -1, + tv: -1 + } + break + default: + return reject(new Error('no support type')) + } - neFetcher.request({ - method: 'post', - url: links[type], - params: encrypt(data) - }).then(res => { - if (res && res.status === 200) { - resolve(res.data) - } else { - reject(new Error(res.statusText)) - } - }).catch(err => { - debug.error(err.message) - }) - }) + neFetcher.request({ + method: 'post', + url: links[type], + params: encrypt(data) + }).then(res => { + if (res && res.status === 200) { + resolve(res.data) + } else { + reject(new Error(res.statusText)) + } + }).catch(err => { + debug.error(err.message) + }) + }) } module.exports = { - neteaseMusic, - fetchNE + neteaseMusic, + fetchNE } diff --git a/server/util/debug.js b/server/util/debug.js index 6cb8b5d..b967823 100644 --- a/server/util/debug.js +++ b/server/util/debug.js @@ -8,42 +8,39 @@ const debug = require('debug') const packageInfo = require('../../package.json') - +const slice = Array.prototype.slice const levelMap = { - success: { - level: 2, - emoji: '✅' - }, - info: { - level: 6, - emoji: '⚡️' - }, - warn: { - level: 3, - emoji: '⚠️' - }, - error: { - level: 1, - emoji: '❌' - } + success: { + level: 2, + emoji: '✅' + }, + info: { + level: 6, + emoji: '⚡️' + }, + warn: { + level: 3, + emoji: '⚠️' + }, + error: { + level: 1, + emoji: '❌' + } } -const slice = Array.prototype.slice module.exports = function getDebug (namespace = '') { - const deBug = debug(`[${packageInfo.name}] ${namespace || ''}`) - function d () { - d.info.apply(d, slice.call(arguments)) - } - - Object.keys(levelMap).map(key => { - d[key] = function () { - deBug.enabled = true - deBug.color = levelMap[key].level - const args = slice.call(arguments) - // args[0] = levelMap[key].emoji + ' ' + args[0] - deBug.apply(null, args) - } - }) - - return d + const deBug = debug(`[${packageInfo.name}] ${namespace || ''}`) + function d () { + d.info.apply(d, slice.call(arguments)) + } + Object.keys(levelMap).map(key => { + d[key] = function () { + deBug.enabled = true + deBug.color = levelMap[key].level + const args = slice.call(arguments) + // args[0] = levelMap[key].emoji + ' ' + args[0] + deBug.apply(null, args) + } + }) + return d } diff --git a/server/util/encrypt.js b/server/util/encrypt.js index e7e6f06..13688c1 100644 --- a/server/util/encrypt.js +++ b/server/util/encrypt.js @@ -12,50 +12,50 @@ const modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b72 const nonce = '0CoJUm6Qyw8W8jud' const pubKey = '010001' -const createSecretKey = (size) => { - const keys = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - let key = "" - for (let i = 0; i < size; i++) { - let pos = Math.random() * keys.length - pos = Math.floor(pos) - key = key + keys.charAt(pos) - } - return key +const createSecretKey = size => { + const keys = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + let key = '' + for (let i = 0; i < size; i++) { + let pos = Math.random() * keys.length + pos = Math.floor(pos) + key = key + keys.charAt(pos) + } + return key } const aesEncrypt = (text, secKey) => { - const _text = text - const lv = new Buffer('0102030405060708', "binary") - const _secKey = new Buffer(secKey, "binary") - const cipher = crypto.createCipheriv('AES-128-CBC', _secKey, lv) - let encrypted = cipher.update(_text, 'utf8', 'base64') - encrypted += cipher.final('base64') - return encrypted + const _text = text + const lv = Buffer.from('0102030405060708', 'binary') + const _secKey = Buffer.from(secKey, 'binary') + const cipher = crypto.createCipheriv('AES-128-CBC', _secKey, lv) + let encrypted = cipher.update(_text, 'utf8', 'base64') + encrypted += cipher.final('base64') + return encrypted } const zfill = (str, size) => { - while (str.length < size) str = "0" + str - return str + while (str.length < size) str = '0' + str + return str } const rsaEncrypt = (text, pubKey, modulus) => { - const _text = text.split('').reverse().join('') - const biText = bigInt(new Buffer(_text).toString('hex'), 16), - biEx = bigInt(pubKey, 16), - biMod = bigInt(modulus, 16), - biRet = biText.modPow(biEx, biMod) - return zfill(biRet.toString(16), 256) + const _text = text.split('').reverse().join('') + const biText = bigInt(Buffer.from(_text).toString('hex'), 16) + const biEx = bigInt(pubKey, 16) + const biMod = bigInt(modulus, 16) + const biRet = biText.modPow(biEx, biMod) + return zfill(biRet.toString(16), 256) } -const encrypt = (params) => { - const text = JSON.stringify(params) - const secKey = createSecretKey(16) - const encText = aesEncrypt(aesEncrypt(text, nonce), secKey) - const encSecKey = rsaEncrypt(secKey, pubKey, modulus) - return { - params: encText, - encSecKey: encSecKey - } +const encrypt = params => { + const text = JSON.stringify(params) + const secKey = createSecretKey(16) + const encText = aesEncrypt(aesEncrypt(text, nonce), secKey) + const encSecKey = rsaEncrypt(secKey, pubKey, modulus) + return { + params: encText, + encSecKey: encSecKey + } } module.exports = encrypt diff --git a/server/util/gravatar.js b/server/util/gravatar.js index c3e99c3..270393f 100644 --- a/server/util/gravatar.js +++ b/server/util/gravatar.js @@ -8,23 +8,21 @@ const gravatar = require('gravatar') const config = require('../config') -const { isEmail } = require('./') - const isProd = process.env.NODE_ENV === 'production' module.exports = (email = '', opt = {}) => { - if (!/^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(email)) { - return config.auth.defaultAvatar - } + if (!/^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(email)) { + return config.auth.defaultAvatar + } - const protocol = `http${isProd ? 's' : ''}` - const url = gravatar.url(email, { - s: '100', - r: 'x', - d: 'retro', - protocol, - ...opt - }) + const protocol = `http${isProd ? 's' : ''}` + const url = gravatar.url(email, { + s: '100', + r: 'x', + d: 'retro', + protocol, + ...opt + }) - return url.replace(`${protocol}://`, 'https://jooger.me/proxy/') + return url.replace(`${protocol}://`, 'https://jooger.me/proxy/') } diff --git a/server/util/index.js b/server/util/index.js index d745038..0e0ff78 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -28,25 +28,25 @@ exports.gravatar = require('./gravatar') exports.noop = function () {} exports.isType = (obj = {}, type = 'Object') => { - if (!Array.isArray(type)) { - type = [type] - } - return type.some(t => { - if (typeof t !== 'string') { - return false - } - return Object.prototype.toString.call(obj) === `[object ${t}]` - }) + if (!Array.isArray(type)) { + type = [type] + } + return type.some(t => { + if (typeof t !== 'string') { + return false + } + return Object.prototype.toString.call(obj) === `[object ${t}]` + }) } exports.createObjectId = (id = '') => { - return id ? mongoose.Types.ObjectId(id) : mongoose.Types.ObjectId() + return id ? mongoose.Types.ObjectId(id) : mongoose.Types.ObjectId() } exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) // 首字母大写 -exports.firstUpperCase = (str = '') => str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) +exports.firstUpperCase = (str = '') => str.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()) // hash 加密 exports.bhash = (str = '') => bcrypt.hashSync(str, 8) @@ -56,36 +56,36 @@ exports.bcompare = (str, hash) => bcrypt.compareSync(str, hash) // 随机字符串 exports.randomString = (length = 8) => { - const chars = `ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz` - let id = '' - for (let i = 0; i < length; i++) { - id += chars[Math.floor(Math.random() * chars.length)] - } - return id + const chars = `ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz` + let id = '' + for (let i = 0; i < length; i++) { + id += chars[Math.floor(Math.random() * chars.length)] + } + return id } exports.getMonthFromNum = (num = 1) => config.constant.monthMap[num - 1] || '' Object.keys(validator).forEach(key => { - exports[key] = function () { - return validator[key].apply(validator, arguments) - } + exports[key] = function () { + return validator[key].apply(validator, arguments) + } }) exports.isSiteUrl = (site = '') => validator.isURL(site, { - protocols: ['http', 'https'], - require_protocol: true + protocols: ['http', 'https'], + require_protocol: true }) // 获取分页请求的响应数据 exports.getDocsPaginationData = (docs = {}) => { - return { - list: docs.docs, - pagination: { - total: docs.total, - current_page: docs.page > docs.pages ? docs.pages : docs.page, - total_page: docs.pages, - per_page: docs.limit - } - } + return { + list: docs.docs, + pagination: { + total: docs.total, + current_page: docs.page > docs.pages ? docs.pages : docs.page, + total_page: docs.pages, + per_page: docs.limit + } + } } diff --git a/server/util/location.js b/server/util/location.js index 7e199c6..5818a78 100644 --- a/server/util/location.js +++ b/server/util/location.js @@ -9,15 +9,15 @@ const geoip = require('geoip-lite') module.exports = (req = {}) => { - const ip = (req.headers['x-forwarded-for'] || - req.headers['x-real-ip'] || - req.connection.remoteAddress || + const ip = (req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress || req.ip || req.ips[0] || '').replace('::ffff:', '') - return { - ip, - location: geoip.lookup(ip) || {} - } + return { + ip, + location: geoip.lookup(ip) || {} + } } diff --git a/server/util/marked.js b/server/util/marked.js index 506f2bb..4e1fce8 100644 --- a/server/util/marked.js +++ b/server/util/marked.js @@ -23,99 +23,99 @@ highlight.registerLanguage('scss', require('highlight.js/lib/languages/scss')) highlight.registerLanguage('sql', require('highlight.js/lib/languages/sql')) highlight.registerLanguage('stylus', require('highlight.js/lib/languages/stylus')) highlight.configure({ - classPrefix: '' // don't append class prefix + classPrefix: '' // don't append class prefix }) const renderer = new marked.Renderer() renderer.heading = function (text, level) { - return `${text}` + return `${text}` } renderer.link = function (href, title, text) { - const isOrigin = href.indexOf('jooger.me') > -1 - const isImage = /(/gi.test(text) - return ` - ${text} - `.replace(/\s+/g, ' ').replace('\n', '') + const isOrigin = href.indexOf('jooger.me') > -1 + const isImage = /(/gi.test(text) + return ` + ${text} + `.replace(/\s+/g, ' ').replace('\n', '') } renderer.image = function (href, title, text) { - return ` - ${text || title || href} - `.replace(/\s+/g, ' ').replace('\n', '') + return ` + ${text || title || href} + `.replace(/\s+/g, ' ').replace('\n', '') } renderer.code = function (code, lang, escaped) { - if (this.options.highlight) { - var out = this.options.highlight(code, lang) - if (out != null && out !== code) { - escaped = true - code = out - } - } + if (this.options.highlight) { + const out = this.options.highlight(code, lang) + if (out != null && out !== code) { + escaped = true + code = out + } + } - const lineCode = code.split('\n') - const codeWrapper = lineCode.map((line, index) => `${line}${index !== lineCode.length - 1 ? '
' : ''}`.replace(/\s+/g, ' ')).join('') + const lineCode = code.split('\n') + const codeWrapper = lineCode.map((line, index) => `${line}${index !== lineCode.length - 1 ? '
' : ''}`.replace(/\s+/g, ' ')).join('') - if (!lang) { - return '
' +
-    codeWrapper +
-      '\n
' - } + if (!lang) { + return '
' +
+		codeWrapper +
+			'\n
' + } - return '
' + '' +
-    codeWrapper +
-    '\n
\n' + return '
' + '' +
+		codeWrapper +
+		'\n
\n' } marked.setOptions({ - renderer, - gfm: true, - pedantic: false, - sanitize: false, - tables: true, - breaks: true, - smartLists: true, - smartypants: true, - highlight (code, lang) { - if (!~languages.indexOf(lang)) { - return highlight.highlightAuto(code).value - } - return highlight.highlight(lang, code).value - } + renderer, + gfm: true, + pedantic: false, + sanitize: false, + tables: true, + breaks: true, + smartLists: true, + smartypants: true, + highlight (code, lang) { + if (!~languages.indexOf(lang)) { + return highlight.highlightAuto(code).value + } + return highlight.highlight(lang, code).value + } }) // 生成文章中的title id function generateId (len) { - const chars = `ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz` - len = len | 8 - let id = '' - for (let i = 0; i < len; i++) { - id += chars[Math.floor(Math.random() * chars.length)] - } - return id + const chars = `ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz` + len = len | 8 + let id = '' + for (let i = 0; i < len; i++) { + id += chars[Math.floor(Math.random() * chars.length)] + } + return id } function escape (html, encode) { - return html - .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') } module.exports = marked diff --git a/server/util/sign-token.js b/server/util/sign-token.js index e58dade..a3b7f31 100644 --- a/server/util/sign-token.js +++ b/server/util/sign-token.js @@ -10,6 +10,6 @@ const jwt = require('jsonwebtoken') const config = require('../config') module.exports = (payload = {}, isLogin = true) => { - const { secrets, session } = config.auth - return jwt.sign(payload, secrets, { expiresIn: isLogin ? session.maxAge : 0 }) + const { secrets, session } = config.auth + return jwt.sign(payload, secrets, { expiresIn: isLogin ? session.maxAge : 0 }) } From bad163862863c735be0430c084e3433d5d433eb4 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Thu, 1 Feb 2018 13:38:54 +0800 Subject: [PATCH 094/208] [update] add pre-git module --- package.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f5b2519..a507296 100644 --- a/package.json +++ b/package.json @@ -81,13 +81,22 @@ "eslint-plugin-node": "^5.2.1", "eslint-plugin-promise": "^3.6.0", "eslint-plugin-standard": "^3.0.1", - "nodemon": "^1.8.1" + "nodemon": "^1.8.1", + "pre-git": "^3.17.0" + }, + "release": { + "analyzeCommits": "simple-commit-message" }, "config": { "pre-git": { + "enabled": true, "pre-commit": [ "npm run precommit" - ] + ], + "post-commit": "git status", + "pre-push": [], + "post-checkout": "", + "post-merge": "" } } } From 39d080a4a3dce3baee923a3b4a601edd3185081d Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Thu, 1 Feb 2018 20:31:44 +0800 Subject: [PATCH 095/208] [update] update --- package.json | 2 +- server/controller/auth.js | 28 +++++++++------------------- server/plugins/crontab.js | 6 ++---- server/service/model-update.js | 5 ++++- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index a507296..2a30d3c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "bin": "./node_modules/.bin/", "scripts": { - "dev": "cross-env NODE_ENV=development nodemon bin/www", + "start": "cross-env NODE_ENV=development nodemon bin/www", "debug": "cross-env NODE_ENV=development nodemon --inspect bin/www", "prod": "cross-env NODE_ENV=production nodemon bin/www", "pm2": "pm2 startOrReload ecosystem.config.js", diff --git a/server/controller/auth.js b/server/controller/auth.js index fd131aa..823bca7 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -8,6 +8,7 @@ // const passport = require('koa-passport') const config = require('../config') +const { userProxy } = require('../proxy') const { UserModel } = require('../model') const { bcompare, getDebug, signToken, proxy, randomString } = require('../util') const debug = getDebug('Auth') @@ -16,21 +17,10 @@ const { getGithubToken, getGithubAuthUserInfo } = require('../service') // const isProd = process.env.NODE_ENV === 'production' exports.localLogin = async (ctx, next) => { - const name = ctx.validateBody('name') - .required('the "name" parameter is required') - .notEmpty() - .isString('the "name" parameter should be String type') - .val() - const password = ctx.validateBody('password') - .required('the "password" parameter is required') - .notEmpty() - .isString('the "password" parameter should be String type') - .val() - - const user = await UserModel.findOne({ name }).catch(err => { - ctx.log.error(err.message) - return null - }) + const name = ctx.validateBody('name').required('缺少登录名').notEmpty().val() + const password = ctx.validateBody('password').required('缺少密码').notEmpty().val() + + const user = await userProxy.findOne({ name }).exec() if (user) { const vertifyPassword = bcompare(password, user.password) @@ -46,12 +36,12 @@ exports.localLogin = async (ctx, next) => { ctx.success({ id: user._id, token - }, 'login success') + }, '登录成功') } else { - ctx.fail(-1, 'incorrect password') + ctx.fail('密码错误') } } else { - ctx.fail(-1, 'user doesn\'t exist') + ctx.fail('用户不存在') } } @@ -64,7 +54,7 @@ exports.logout = async (ctx, next) => { ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) ctx.cookies.set(config.auth.userCookieKey, ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) debug.success('登出成功, 用户ID:%s,用户名:%s', ctx.user._id, ctx.user.name) - ctx.success(null, 'logout success') + ctx.success(null, '登出成功') } exports.info = async (ctx, next) => { diff --git a/server/plugins/crontab.js b/server/plugins/crontab.js index a97d4f7..6915356 100644 --- a/server/plugins/crontab.js +++ b/server/plugins/crontab.js @@ -10,14 +10,12 @@ const { getDebug } = require('../util') const { modelUpdate } = require('../service') const debug = getDebug('Crontab') -exports.start = () => { +exports.start = () => setTimeout(() => { // 友链 每1小时更新一次 modelUpdate.updateOption() setInterval(modelUpdate.updateOption, 1000 * 60 * 60 * 1) - // 用户 每1天更新一次 modelUpdate.updateGithubInfo() setInterval(modelUpdate.updateGithubInfo, 1000 * 60 * 60 * 24) - debug.success('定时任务启动成功') -} +}, 0) diff --git a/server/service/model-update.js b/server/service/model-update.js index e569a9a..2969e3b 100644 --- a/server/service/model-update.js +++ b/server/service/model-update.js @@ -61,6 +61,7 @@ exports.updateGithubInfo = async () => { const updates = await getGithubUsersInfo(githubUsers.map(user => user.github.login)) Promise.all( updates.reduce((tasks, data, index) => { + if (!data) return tasks const user = githubUsers[index] const u = { name: data.name, @@ -84,7 +85,9 @@ exports.updateGithubInfo = async () => { return tasks }, []) ).then(() => { - debug.success('所有Github用户信息更新成功') + debug.success('Github用户信息全部更新成功') + }).catch(err => { + debug.error(err.message) }) } From 13c5b94e401cfbb3e846013695b3c30f5831c241 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Wed, 7 Feb 2018 11:48:04 +0800 Subject: [PATCH 096/208] [feature] add statistics api --- server/controller/statistics.js | 27 +++++++++++++++++++++++++-- server/middleware/header.js | 2 +- server/proxy/base.js | 10 +++++++--- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/server/controller/statistics.js b/server/controller/statistics.js index 94567a4..7acc3aa 100644 --- a/server/controller/statistics.js +++ b/server/controller/statistics.js @@ -1,8 +1,31 @@ /** * @desc Statistics controller * @author Jooger - * @date 25 Sep 2017 + * @date 2 Feb 2018 */ +const { articleProxy, categoryProxy, tagProxy, userProxy, commentProxy } = require('../proxy') + // TODO: 站内统计 -exports.data = async (ctx, next) => {} +exports.data = async (ctx, next) => { + const data = await Promise.all([ + articleProxy.count({ state: 1 }).exec(), + categoryProxy.count().exec(), + tagProxy.count().exec(), + userProxy.count().nor({ role: 0 }).exec(), + commentProxy.count({ type: 0 }).exec(), + commentProxy.count({ type: 1 }).exec() + ]).then(([articles, categories, tags, users, comments, guestComments]) => { + return { + article: articles, + category: categories, + tag: tags, + user: users, + comment: comments, + guestComment: guestComments + } + }) + data + ? ctx.success(data, '统计信息获取成功') + : ctx.fail('统计信息获取失败') +} diff --git a/server/middleware/header.js b/server/middleware/header.js index bdd1a32..a6bbea4 100644 --- a/server/middleware/header.js +++ b/server/middleware/header.js @@ -19,7 +19,7 @@ module.exports = async (ctx, next) => { if (allowed) { response.set('Access-Control-Allow-Origin', origin) } - response.set('Access-Control-Allow-Headers', 'Authorization, Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With') + response.set('Access-Control-Allow-Headers', 'token, Authorization, Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With') response.set('Access-Control-Allow-Methods', 'PUT,PATCH,POST,GET,DELETE,OPTIONS') response.set('Access-Control-Allow-Credentials', true) response.set('Content-Type', 'application/json;charset=utf-8') diff --git a/server/proxy/base.js b/server/proxy/base.js index 11fa89d..f07903f 100644 --- a/server/proxy/base.js +++ b/server/proxy/base.js @@ -55,15 +55,15 @@ module.exports = class BaseProxy { }) } - del (query) { + del (query = {}) { return this.Model.remove(query) } - delById (id) { + delById (id = '') { return this.del({ _id: id }) } - delByIds (ids) { + delByIds (ids = []) { return this.del({ _id: { $in: ids @@ -74,4 +74,8 @@ module.exports = class BaseProxy { aggregate (opt = {}) { return this.Model.aggregate(opt) } + + count (query = {}) { + return this.Model.count(query) + } } From 2e70d65f53f7cccddc7f38ea149a1d45eec40571 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Wed, 7 Feb 2018 20:30:07 +0800 Subject: [PATCH 097/208] [update] update params --- server/config/index.js | 2 +- server/util/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index 474af62..65667e9 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -21,7 +21,7 @@ const baseConfig = { port: process.env.PORT || 3001, // 限制参数 limit: { - articleLimit: 3, + articleLimit: 15, // 相关文章限制个数 relatedArticleLimit: 10, hotLimit: 7, diff --git a/server/util/index.js b/server/util/index.js index 0e0ff78..b4b8c04 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -83,7 +83,7 @@ exports.getDocsPaginationData = (docs = {}) => { list: docs.docs, pagination: { total: docs.total, - current_page: docs.page > docs.pages ? docs.pages : docs.page, + cur_page: docs.page > docs.pages ? docs.pages : docs.page, total_page: docs.pages, per_page: docs.limit } From acf81f6326ded4e78d509973c093e2a1230b3f21 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Sun, 11 Feb 2018 10:32:30 +0800 Subject: [PATCH 098/208] [feature] supports Docker, upgrade to v1.5.0 --- . dockerignore | 7 +++++++ .gitignore | 1 - Dockerfile | 20 ++++++++++++++++++++ README.md | 6 +++++- docker-start.sh | 12 ++++++++++++ package.json | 11 +++-------- server/app.js | 5 +---- server/config/production.js | 7 ++++++- server/plugins/gc.js | 26 -------------------------- server/plugins/index.js | 1 - server/service/model-update.js | 1 + 11 files changed, 55 insertions(+), 42 deletions(-) create mode 100644 . dockerignore create mode 100644 Dockerfile create mode 100644 docker-start.sh delete mode 100644 server/plugins/gc.js diff --git a/. dockerignore b/. dockerignore new file mode 100644 index 0000000..00cd03f --- /dev/null +++ b/. dockerignore @@ -0,0 +1,7 @@ +.DS_Store +npm-debug.log* +package-lock.json +.nuxt/ + +.vscode +.idea \ No newline at end of file diff --git a/.gitignore b/.gitignore index 06a2ae7..a838a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,3 @@ package-lock.json # pm2 ecosystem.config.js -process.js diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a8f9f13 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:latest + +LABEL author="Jooger " + +RUN npm install pm2 -g + +ADD . /www/app/node-server/ + +WORKDIR /www/app/node-server + +RUN npm i + +ENV HOST 0.0.0.0 +ENV PORT 3001 +ENV NODE_ENV production + +RUN ["chmod", "+x", "/www/app/node-server/docker-start.sh"] +CMD /bin/bash /www/app/node-server/docker-start.sh $NODE_ENV + +EXPOSE 3001 diff --git a/README.md b/README.md index 87afb36..07ac823 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,9 @@ node-server * TypeScript升级 -* ESlint +* ~~ESlint~~ (2018.02.01) + +* ~~Docker支持~~ * 邮件模板 @@ -123,3 +125,5 @@ node-server * 完善API文档 * 测试case + +* GraphQL diff --git a/docker-start.sh b/docker-start.sh new file mode 100644 index 0000000..2329c22 --- /dev/null +++ b/docker-start.sh @@ -0,0 +1,12 @@ +#!bin/sh + +NODE_ENV=$1 + +if [ -z $NODE_ENV ] +then echo "请输入环境" +exit 1 +fi + +echo $NODE_ENV + +pm2 startOrReload ecosystem.config.js --env $NODE_ENV --no-daemon diff --git a/package.json b/package.json index 2a30d3c..7c876e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "1.4.0", + "version": "1.5.0", "private": true, "description": "🔥 My blog's api server build by koa2 and mongoose", "homepage": "https://github.com/jo0ger/node-server", @@ -31,7 +31,7 @@ "debug": "cross-env NODE_ENV=development nodemon --inspect bin/www", "prod": "cross-env NODE_ENV=production nodemon bin/www", "pm2": "pm2 startOrReload ecosystem.config.js", - "pm2:prod": "pm2 startOrReload ecosystem.config.js --env production", + "prod:pm2": "pm2 startOrReload ecosystem.config.js --env production", "deploy": "pm2 deploy ecosystem.config.js production", "precommit": "npm run lint", "lint": "eslint --ext .js,.ts --ignore-path .gitignore ." @@ -44,11 +44,9 @@ "crypto": "^1.0.1", "debug": "^2.6.9", "formidable": "^1.1.1", - "gc-stats": "^1.0.2", "geoip-lite": "^1.2.1", "gravatar": "^1.6.0", "highlight.js": "^9.12.0", - "idle-gc": "^1.0.1", "jsonwebtoken": "^8.1.0", "koa": "^2.4.1", "koa-bodyparser": "^3.2.0", @@ -93,10 +91,7 @@ "pre-commit": [ "npm run precommit" ], - "post-commit": "git status", - "pre-push": [], - "post-checkout": "", - "post-merge": "" + "post-commit": "git status" } } } diff --git a/server/app.js b/server/app.js index 6901bc1..3e6b0e0 100644 --- a/server/app.js +++ b/server/app.js @@ -20,7 +20,7 @@ const packageInfo = require('../package.json') const middlewares = require('./middleware') const routes = require('./routes') const config = require('./config') -const { mongo, redis, akismet, validation, mailer, gc, crontab } = require('./plugins') +const { mongo, redis, akismet, validation, mailer, crontab } = require('./plugins') const app = new Koa() app.keys = config.auth.secrets @@ -70,7 +70,4 @@ mailer.start() // crontab crontab.start() -// v8 gc -gc.start() - module.exports = app diff --git a/server/config/production.js b/server/config/production.js index 6d2b781..df8eab4 100644 --- a/server/config/production.js +++ b/server/config/production.js @@ -6,9 +6,14 @@ 'use strict' +const isDocker = process.env.RUN_ENV === 'docker' + module.exports = { mongo: { - uri: 'mongodb://127.0.0.1/jooger-me' + uri: `mongodb://${isDocker ? 'mongo' : '127.0.0.1'}/jooger-me` + }, + redis: { + host: isDocker ? 'redis' : '127.0.0.1' }, auth: { session: { diff --git a/server/plugins/gc.js b/server/plugins/gc.js deleted file mode 100644 index b3fc613..0000000 --- a/server/plugins/gc.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @desc V8 GC - * @author Jooger - * @date 30 Oct 2017 - */ - -'use strict' - -const gc = require('idle-gc') -const gcStat = require('gc-stats') -const debug = require('../util').getDebug('GC') -const isProd = process.env.NODE_ENV === 'production' - -exports.start = (interval = 5000, delay = 5000) => { - if (isProd) { - setTimeout(() => { - gcStat().on('stats', stats => { - debug('回收完毕,用时 %s ms,共回收 %s KB堆内存', stats.pauseMS, stats.diff.totalHeapSize / 1000) - }) - gc.start(interval) - debug.success('服务启动成功') - }, delay) - } -} - -exports.stop = () => gc.stop() diff --git a/server/plugins/index.js b/server/plugins/index.js index a6b5ed7..d8b88d3 100644 --- a/server/plugins/index.js +++ b/server/plugins/index.js @@ -11,5 +11,4 @@ exports.redis = require('./redis') exports.akismet = require('./akismet') exports.validation = require('./validation') exports.mailer = require('./mailer') -exports.gc = require('./gc') exports.crontab = require('./crontab') diff --git a/server/service/model-update.js b/server/service/model-update.js index 2969e3b..ecb64b8 100644 --- a/server/service/model-update.js +++ b/server/service/model-update.js @@ -27,6 +27,7 @@ exports.updateOption = async (option = null) => { debug.error('数据查找失败,错误:', err.message) return {} }) + if (!option) return } // 更新友链 From 819c39222451f5cec89bc0e6bbf745cce81f4345 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Sun, 11 Feb 2018 20:29:36 +0800 Subject: [PATCH 099/208] [update] add aliyun oss params api --- server/config/index.js | 8 ++++++++ server/controller/aliyun.js | 21 +++++++++++++++++++++ server/controller/index.js | 1 + server/routes/backend.js | 7 +++++-- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 server/controller/aliyun.js diff --git a/server/config/index.js b/server/config/index.js index 65667e9..4c79dc6 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -69,6 +69,14 @@ const baseConfig = { akismet: { apiKey: process.env.akismetApikey || 'akismet api key' }, + aliyun: { + oss: { + accessKeyId: process.env.aliyunOssAccessKeyId || 'alayu accesskey Id', + accessKeySecret: process.env.aliyunOssAccessKeySecret || 'aliyun oss accesskey secret', + bucket: 'jooger-static', + region: 'oss-cn-beijing' + } + }, constant: { // 允许请求的域名 allowedOrigins: [ diff --git a/server/controller/aliyun.js b/server/controller/aliyun.js new file mode 100644 index 0000000..0de64b9 --- /dev/null +++ b/server/controller/aliyun.js @@ -0,0 +1,21 @@ +/** + * @desc Aliyun controller + * @author Jooger + * @date 26 Sep 2017 + */ + +'use strict' + +const config = require('../config') +const oss = config.aliyun.oss + +exports.oss = async (ctx, next) => { + oss + ? ctx.success({ + accessKeyId: 'LTAIMh28MLnWG7MA', + accessKeySecret: 'B0v7JCx65VmtNws22BzFnTQcX2kzm9', + bucket: oss.bucket, + region: oss.region + }, '阿里云OSS参数获取成功') + : ctx.fail('阿里云OSS参数获取失败') +} diff --git a/server/controller/index.js b/server/controller/index.js index 9f7c9a2..c0ce76e 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -16,3 +16,4 @@ exports.user = require('./user') exports.auth = require('./auth') exports.moment = require('./moment') exports.statistics = require('./statistics') +exports.aliyun = require('./aliyun') diff --git a/server/routes/backend.js b/server/routes/backend.js index b5b81de..706344f 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -17,7 +17,8 @@ const { auth, music, statistics, - moment + moment, + aliyun } = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() @@ -83,7 +84,9 @@ router.patch('/moments/:id', isAuthenticated, moment.update) router.delete('/moments/:id', isAuthenticated, moment.delete) // Statistics -// TODO: +// OPTIMIZE: router.get('/statistics', isAuthenticated, statistics.data) +router.get('/aliyun/oss', isAuthenticated, aliyun.oss) + module.exports = router From c75b69807d17b75c3c55d4e721225aedd2094361 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Mon, 12 Feb 2018 17:52:07 +0800 Subject: [PATCH 100/208] [feature] add notification api --- README.md | 14 +- package.json | 1 - server/app.js | 14 +- server/config/index.js | 25 +++ server/controller/article.js | 26 ++- server/controller/auth.js | 41 ++--- server/controller/comment.js | 261 +++++++++++++--------------- server/controller/index.js | 1 + server/controller/moment.js | 85 ++++----- server/controller/notification.js | 83 +++++++++ server/controller/statistics.js | 2 +- server/controller/user.js | 4 + server/middleware/authenticate.js | 61 ------- server/model/schema/article.js | 6 +- server/model/schema/comment.js | 2 +- server/model/schema/index.js | 2 +- server/model/schema/log.js | 18 -- server/model/schema/notification.js | 39 +++++ server/plugins/mailer.js | 2 +- server/proxy/article.js | 24 +++ server/proxy/comment.js | 38 ++++ server/proxy/index.js | 3 +- server/proxy/notification.js | 39 +++++ server/proxy/user.js | 46 +++++ server/routes/backend.js | 10 +- server/service/github-passport.js | 88 ---------- server/service/index.js | 1 - 27 files changed, 512 insertions(+), 424 deletions(-) create mode 100644 server/controller/notification.js delete mode 100644 server/model/schema/log.js create mode 100644 server/model/schema/notification.js create mode 100644 server/proxy/notification.js delete mode 100644 server/service/github-passport.js diff --git a/README.md b/README.md index 07ac823..6a13903 100644 --- a/README.md +++ b/README.md @@ -100,25 +100,23 @@ node-server * ~~评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer)~~ (2017.10.29) -* ~~GC优化~~ (2017.10.30,linux下需要预先安装g++) +* ~~GC优化~~ (2017.10.30,linux下需要预先安装g++, **已废弃**) * ~~个人动态api~~ (2017.10.30) * ~~文章归档api~~(2018.01.04) -* model代理 - -* TypeScript升级 +* ~~Model代理~~ (2018.01.28) * ~~ESlint~~ (2018.02.01) -* ~~Docker支持~~ +* ~~Docker支持~~ (2018.02.09) -* 邮件模板 +* ~~站内通知api~~ (2018.02.12) -* 消息api +* 邮件模板 -* 日志api +* TypeScript升级 * 统计api diff --git a/package.json b/package.json index 7c876e7..fbf9662 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "koa-json": "^2.0.2", "koa-logger": "^2.0.1", "koa-onerror": "^1.2.1", - "koa-passport": "^4.0.0", "koa-router": "^7.1.1", "koa-session": "^5.5.0", "lodash": "^4.17.4", diff --git a/server/app.js b/server/app.js index 3e6b0e0..8d8c4ff 100644 --- a/server/app.js +++ b/server/app.js @@ -12,7 +12,6 @@ const logger = require('koa-logger') const onerror = require('koa-onerror') const bouncer = require('koa-bouncer') const session = require('koa-session') -const passport = require('koa-passport') const compress = require('koa-compress') const bodyparser = require('koa-bodyparser') const koaBunyanLogger = require('koa-bunyan-logger') @@ -21,6 +20,7 @@ const middlewares = require('./middleware') const routes = require('./routes') const config = require('./config') const { mongo, redis, akismet, validation, mailer, crontab } = require('./plugins') +const isProd = process.env.NODE_ENV === 'production' const app = new Koa() app.keys = config.auth.secrets @@ -50,7 +50,6 @@ app.use(middlewares.error) // form parse // app.use(middlewares.formidable()) app.use(session(config.auth.session, app)) -app.use(passport.initialize()) app.use(compress()) // routes @@ -61,11 +60,14 @@ mongo.connect() // connect redis redis.connect() -// akismet -akismet.start() -// mailer -mailer.start() +if (isProd) { + // akismet + akismet.start() + + // mailer + mailer.start() +} // crontab crontab.start() diff --git a/server/config/index.js b/server/config/index.js index 4c79dc6..23e7378 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -99,6 +99,31 @@ const baseConfig = { GITHUB_USER: 2 }, monthMap: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + // 站内通知 + notification: { + typeMap: { + GENERAL: 0, + COMMENT: 1, + LIKE: 2, + USER: 3 + }, + categoryMap: { + // type === 0,系统通知 + MUTE_USER: 'mute-user', // 用户禁言 + // type === 1,评论通知 + COMMENT_COMMENT: 'comment-comment', // 评论(非回复) + COMMENT_REPLY: 'comment-reply', // 评论回复 + COMMENT_UPDATE: 'comment-update', // 评论更新 + // type === 2,点赞通知 + LIKE_ARTICLE: 'like-article', // 文章点赞 + UNLIKE_ARTICLE: 'unlike-article', // 文章取消点赞 + LIKE_COMMENT: 'like-comment', // 评论点赞 + UNLIKE_COMMENT: 'unlike-comment', // 评论取消点赞 + // type === 3, 用户操作通知 + USER_CREATE: 'user-create', // 用户创建 + USER_UPDATE: 'user-update' // 用户更新 + } + }, redisCacheKey: { music: 'music-data' } diff --git a/server/controller/article.js b/server/controller/article.js index 68424fb..cd971f2 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -7,7 +7,7 @@ 'use strict' const config = require('../config') -const { articleProxy, categoryProxy, tagProxy } = require('../proxy') +const { articleProxy, categoryProxy, tagProxy, userProxy } = require('../proxy') const { marked, isObjectId, createObjectId, getMonthFromNum, getDocsPaginationData } = require('../util') // 文章列表 @@ -292,11 +292,25 @@ exports.delete = async (ctx, next) => { exports.like = async (ctx, next) => { const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() - const data = await articleProxy.updateById(id, { - $inc: { - 'meta.ups': like ? 1 : -1 - } - }).exec() + const user = ctx.validateBody('user').optional().isObjectId().val() + let userCache = null + if (user) { + userCache = await userProxy.getById(user).exec().catch(err => { + ctx.log.error(err.message) + return null + }) + } + + let data = null + if (!userCache || !!userCache.role) { + data = await articleProxy.likeAndNotify(id, like, userCache) + } else { + data = await articleProxy.updateById(id, { + $inc: { + 'meta.ups': like ? 1 : -1 + } + }).exec() + } data ? ctx.success(null, '文章点赞成功') diff --git a/server/controller/auth.js b/server/controller/auth.js index 823bca7..25483b5 100644 --- a/server/controller/auth.js +++ b/server/controller/auth.js @@ -6,15 +6,11 @@ 'use strict' -// const passport = require('koa-passport') const config = require('../config') const { userProxy } = require('../proxy') -const { UserModel } = require('../model') const { bcompare, getDebug, signToken, proxy, randomString } = require('../util') -const debug = getDebug('Auth') const { getGithubToken, getGithubAuthUserInfo } = require('../service') -// const debugGithub = getDebug('Github:Auth') -// const isProd = process.env.NODE_ENV === 'production' +const debug = getDebug('Auth') exports.localLogin = async (ctx, next) => { const name = ctx.validateBody('name').required('缺少登录名').notEmpty().val() @@ -66,7 +62,7 @@ exports.info = async (ctx, next) => { if (ctx._isSnsAuthenticated) { // TODO: 第三方信息获取 } else if (ctx._isAuthenticated) { - data = await UserModel.findById(adminId) + data = await userProxy.getById(adminId) .select('-password') .exec() .catch(err => { @@ -85,25 +81,6 @@ exports.info = async (ctx, next) => { } } -// github login -// exports.githubLogin = async (ctx, next) => { -// await passport.authenticate('github', { -// session: false -// }, (err, user) => { -// debugGithub('Github权限验证回调处理开始') -// const redirectUrl = ctx.session.passport.redirectUrl - -// const { session } = config.auth -// const opt = { signed: false, maxAge: session.maxAge, httpOnly: false } -// opt.domain = session.domain || null -// ctx.cookies.set(config.sns.github.key, user.token, opt) -// ctx.cookies.set(config.auth.userCookieKey, user._id, opt) - -// debugGithub.success('Github权限验证回调处理成功, 用户ID:%s,用户名:%s', user._id, user.name) -// return ctx.redirect(redirectUrl) -// })(ctx) -// } - exports.fetchGithubToken = async (ctx, next) => { const code = ctx.validateQuery('code').required('缺少code参数').toString().val() const token = await getGithubToken(code) @@ -129,7 +106,7 @@ exports.fetchGithubUser = async (ctx, next) => { } async function createLocalUserFromGithub (githubUser) { - const user = await UserModel.findOne({ + const user = await userProxy.findOne({ 'github.id': githubUser.id }).catch(err => { debug.error('本地用户查找失败, 错误:', err.message) @@ -143,7 +120,7 @@ async function createLocalUserFromGithub (githubUser) { github: githubUser, role: user.role } - const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData) + const updatedUser = await userProxy.updateById(user._id, userData) .select('-password -role -createdAt -updatedAt') .exec().catch(err => { debug.error('本地用户更新失败, 错误:', err.message) @@ -159,7 +136,7 @@ async function createLocalUserFromGithub (githubUser) { role: 1 } - const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { + const checkUser = await userProxy.findOne({ name: newUser.name }).exec().catch(err => { debug.error('本地用户查找失败, 错误:', err.message) return true }) @@ -168,7 +145,11 @@ async function createLocalUserFromGithub (githubUser) { newUser.name += '-' + randomString() } - const data = await new UserModel(newUser).save().catch(err => debug.error('本地用户创建失败, 错误:', err.message)) - return data ? data.toObject() : null + const data = await userProxy.createAndNotify(newUser).catch(err => { + debug.error('本地用户创建失败, 错误:', err.message) + return null + }) + + return data && data.length ? data[0].toObject() : null } } diff --git a/server/controller/comment.js b/server/controller/comment.js index c3276e7..6124b9d 100644 --- a/server/controller/comment.js +++ b/server/controller/comment.js @@ -8,28 +8,29 @@ const config = require('../config') const { akismet, mailer } = require('../plugins') -const { CommentModel, UserModel, ArticleModel } = require('../model') -const { isType, marked, isObjectId, createObjectId, getDebug, getLocation, gravatar } = require('../util') +const { commentProxy, userProxy, articleProxy } = require('../proxy') +const { isType, marked, isObjectId, createObjectId, getDebug, getLocation, gravatar, getDocsPaginationData } = require('../util') const debug = getDebug('Comment') const isProd = process.env.NODE_ENV === 'development' +// 评论列表 exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.commentLimit).toInt().gt(0, '每页评论数量必须大于0').val() - const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() - const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() - const type = ctx.validateQuery('type').optional().toInt().isIn([0, 1], '评论类型参数无效').val() - const author = ctx.validateQuery('author').optional().toString().isObjectId('用户ID参数无效').val() - const article = ctx.validateQuery('article').optional().toString().isObjectId('文章ID参数无效').val() + const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.commentLimit).toInt().gt(0, 'per_page参数必须大于0').val() + const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'page参数必须大于0').val() + const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'state参数错误').val() + const type = ctx.validateQuery('type').optional().toInt().isIn([0, 1], 'type参数错误').val() + const author = ctx.validateQuery('author').optional().toString().isObjectId().val() + const article = ctx.validateQuery('article').optional().toString().isObjectId().val() const keyword = ctx.validateQuery('keyword').optional().toString().val() - const parent = ctx.validateQuery('parent').optional().toString().isObjectId('父评论ID参数无效').val() + const parent = ctx.validateQuery('parent').optional().toString().isObjectId().val() // 时间区间查询仅后台可用,且依赖于createdAt const startDate = ctx.validateQuery('start_date').optional().toString().val() const endDate = ctx.validateQuery('end_date').optional().toString().val() // 排序仅后台能用,且order和sortBy需同时传入才起作用 // -1 desc | 1 asc - const order = ctx.validateQuery('order').optional().toInt().isIn([-1, 1], '排序方式参数无效').val() + const order = ctx.validateQuery('order').optional().toInt().isIn([-1, 1], 'order参数错误').val() // createdAt | updatedAt | ups - const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn(['createdAt', 'updatedAt', 'ups'], '排序项参数无效').val() + const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn(['createdAt', 'updatedAt', 'ups'], 'sort_by参数错误').val() // 过滤条件 const options = { @@ -89,7 +90,7 @@ exports.list = async (ctx, next) => { query.author = author } else { // 普通字符串,需要先查到id - const u = await UserModel.findOne({ name: author }).exec() + const u = await userProxy.findOne({ name: author }).exec() .catch(err => { ctx.log.error(err.message) return null @@ -105,7 +106,7 @@ exports.list = async (ctx, next) => { query.article = article } else { // 普通字符串,需要先查到id - const a = await ArticleModel.findOne({ name: article }).exec() + const a = await articleProxy.findOne({ name: article }).exec() .catch(err => { ctx.log.error(err.message) return null @@ -153,10 +154,7 @@ exports.list = async (ctx, next) => { } } - const comments = await CommentModel.paginate(query, options).catch(err => { - ctx.log.error(err.message) - return null - }) + const comments = await commentProxy.paginate(query, options) if (comments) { const data = [] @@ -165,7 +163,7 @@ exports.list = async (ctx, next) => { doc = doc.toObject() doc.subCount = 0 data.push(doc) - return CommentModel.count({ parent: doc._id }).exec() + return commentProxy.count({ parent: doc._id }).exec() .then(count => { doc.subCount = count }) @@ -174,27 +172,21 @@ exports.list = async (ctx, next) => { doc.subCount = 0 }) })) - ctx.success({ - list: data, - pagination: { - total: comments.total, - current_page: comments.page > comments.pages ? comments.pages : comments.page, - total_page: comments.pages, - per_page: comments.limit - } - }) + comments.docs = data + ctx.success(getDocsPaginationData(comments), '评论列表获取成功') } else { - ctx.fail(-1) + ctx.fail('评论列表获取失败') } } +// 评论详情 exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + const id = ctx.validateParam('id').required('缺少评论ID').toString().isObjectId().val() let data = null let queryPs = null if (!ctx._isAuthenticated) { - queryPs = CommentModel.findById(id, { state: 1, spam: false }) + queryPs = commentProxy.getById(id, { state: 1, spam: false }) .select('-content -state -updatedAt -type -spam') .populate({ path: 'author', @@ -209,44 +201,50 @@ exports.item = async (ctx, next) => { select: 'author meta sticky ups' }) } else { - queryPs = CommentModel.findById(id) - } - - data = await queryPs.exec().catch(err => { - ctx.log.error(err.message) - return null - }) + queryPs = commentProxy.getById(id) + } + + data = await queryPs.populate([ + { + path: 'author', + select: 'github' + }, { + path: 'parent', + select: 'author meta sticky ups' + }, { + path: 'forward', + select: 'author meta sticky ups' + } + ]).exec() - if (data) { - data = data.toObject() - ctx.success(data) - } else { - ctx.fail('评论不存在') - } + data + ? ctx.success(data.toObject(), '评论详情获取成功') + : ctx.fail('评论详情获取失败') } +// 发表评论 exports.create = async (ctx, next) => { - const content = ctx.validateBody('content').required('内容参数必填').notEmpty().val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '评论状态参数无效').val() - const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], '置顶参数无效').val() - const type = ctx.validateBody('type').defaultTo(0).toInt().isIn([0, 1], '评论类型参数无效').val() - const article = ctx.validateBody('article').optional().toString().isObjectId('文章ID参数无效').val() - const parent = ctx.validateBody('parent').optional().toString().isObjectId('父评论ID参数无效').val() - const forward = ctx.validateBody('forward').optional().toString().isObjectId('前置评论ID参数无效').val() + const content = ctx.validateBody('content').required('缺少评论内容').notEmpty().val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数错误').val() + const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], 'sticky参数错误').val() + const type = ctx.validateBody('type').defaultTo(0).toInt().isIn([0, 1], 'type参数错误').val() + const article = ctx.validateBody('article').optional().toString().isObjectId('article参数错误').val() + const parent = ctx.validateBody('parent').optional().toString().isObjectId('parent参数错误').val() + const forward = ctx.validateBody('forward').optional().toString().isObjectId('forward参数错误').val() // ObjectId | { id, name, email, site } - const author = ctx.validateBody('author').required('作者参数无效').val() + const author = ctx.validateBody('author').required('author参数错误').val() const req = ctx.req const comment = { content } if (type === undefined || type === 0) { if (!article) { - return ctx.fail('缺少文章ID参数') + return ctx.fail('缺少article参数') } comment.article = article } if ((parent && !forward) || (!parent && forward)) { - return ctx.fail('父评论ID和前置评论ID必须同时存在') + return ctx.fail('缺少parent和forward参数') } const user = await checkAuthor.call(ctx, author) @@ -254,13 +252,14 @@ exports.create = async (ctx, next) => { return ctx.fail('作者不存在') } else if (user.mute) { // 如果被禁言 - return ctx.fail('您已经被禁言') + return ctx.fail('你已经被禁言') } comment.author = user._id if (!checkUserSpam(user)) { - return ctx.fail('您的垃圾评论数量已达到最大限制,已被禁言') + return ctx.fail('你的垃圾评论数量已达到最大限制,已被禁言') } + const isAdmin = !user.role if (state !== undefined) { comment.state = state @@ -310,13 +309,10 @@ exports.create = async (ctx, next) => { forward && (comment.forward = forward) comment.renderedContent = marked(content) - let data = await new CommentModel(comment).save().catch(err => { - ctx.log.error(err.message) - return null - }) + let data = await commentProxy[isAdmin ? 'newAndSave' : 'createAndNotify'](comment) if (data) { - let p = CommentModel.findById(data._id) + let p = commentProxy.getById(data._id) if (!ctx._isAuthenticated) { p = p.select('-content -state -updatedAt') .populate({ @@ -338,7 +334,7 @@ exports.create = async (ctx, next) => { ctx.log.error(err.message) return null }) - ctx.success(data, '评论成功') + ctx.success(data, '评论创建成功') // 如果是文章评论,则更新文章评论数量 if (type === 0) { updateArticleCommentCount([comment.article]) @@ -346,17 +342,19 @@ exports.create = async (ctx, next) => { // 发送邮件通知站主和被评论者 sendEmailToAdminAndUser(data, permalink) } else { - ctx.fail('评论失败') + ctx.fail('评论创建失败') } } +// 评论更新 exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() - const content = ctx.validateBody('content').optional().isString('内容参数必须是字符串类型').val() - const state = ctx.validateBody('state').optional().toInt().isIn([-2, 0, 1, 2], '评论状态参数无效').val() - const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], '置顶参数无效').val() + const id = ctx.validateParam('id').required('评论ID参数错误').toString().isObjectId().val() + const content = ctx.validateBody('content').optional().val() + const state = ctx.validateBody('state').optional().toInt().isIn([-2, 0, 1, 2], 'state参数错误').val() + const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], 'sticky参数错误').val() const comment = {} - let cache = await CommentModel.findById(id) + + let cache = await commentProxy.getById(id) .populate('author') .exec() if (!cache) { @@ -411,7 +409,7 @@ exports.update = async (ctx, next) => { } } - let p = CommentModel.findByIdAndUpdate(id, comment, { new: true }) + let p = commentProxy.updateById(id, comment) if (!ctx._isAuthenticated) { p = p.select('-content -state -updatedAt') .populate({ @@ -427,49 +425,49 @@ exports.update = async (ctx, next) => { select: 'author meta sticky ups' }) } - const data = await p.exec().catch(err => { - ctx.log.error(err.message) - return null - }) - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + const data = await p.exec() + data + ? ctx.success(data, '评论更新成功') + : ctx.fail('评论更新失败') } +// 删除评论 exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() - const data = await CommentModel.remove({ _id: id }).catch(err => { - ctx.log.error(err.message) - return null - }) + const id = ctx.validateParam('id').required('缺少评论ID').toString().isObjectId().val() + const data = await commentProxy.delById(id).exec() - if (data && data.result && data.result.ok) { - ctx.success() - } else { - ctx.fail('评论不存在') - } + data && data.result && data.result.ok + ? ctx.success(null, '评论删除成功') + : ctx.fail('评论删除失败') } +// 评论点赞 exports.like = async (ctx, next) => { - const id = ctx.validateParam('id').required('评论ID参数无效').toString().isObjectId('评论ID参数无效').val() + const id = ctx.validateParam('id').required('评论ID参数错误').toString().isObjectId('评论ID参数错误').val() const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() + const user = ctx.validateBody('user').optional().isObjectId().val() + let userCache = null + if (user) { + userCache = await userProxy.getById(user).exec().catch(err => { + ctx.log.error(err.message) + return null + }) + } - const data = await CommentModel.findByIdAndUpdate(id, { - $inc: { - ups: like ? 1 : -1 - } - }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - ctx.success() + let data = null + if (!userCache || !!userCache.role) { + data = await commentProxy.likeAndNotify(id, like, userCache) } else { - ctx.fail('评论不存在') + data = await commentProxy.updateById(id, { + $inc: { + ups: like ? 1 : -1 + } + }).exec() } + + data + ? ctx.success(null, '评论点赞成功') + : ctx.fail('评论点赞失败') } // 获取永久链接 @@ -499,9 +497,7 @@ function getCommentType (type) { // 检测用户以往spam评论 async function checkUserSpam (user) { - const userComments = await CommentModel.find({ - author: user._id - }) + const userComments = await commentProxy.find({ author: user._id }) .exec() .catch(err => { debug.error('用户历史评论获取失败,错误:', err.message) @@ -513,16 +509,9 @@ async function checkUserSpam (user) { if (spamComments.length >= config.limit.commentSpamLimit) { if (!user.mute) { // 将用户禁言 - await UserModel.update({ _id: user._id }, { - mute: true - }) - .exec() - .then(() => { - debug.success('用户禁言成功,用户:', user.name) - }) - .catch(err => { - debug.error('用户禁言失败,请手动禁言,错误:', err.message) - }) + await userProxy.muteByIdAndNotify(user._id) + .then(() => debug.success('用户禁言成功,用户:', user.name)) + .catch(err => debug.error('用户禁言失败,请手动禁言,错误:', err.message)) } return false } @@ -536,7 +525,7 @@ async function updateArticleCommentCount (articleIds = []) { } // TIP: 这里必须$in的是一个ObjectId对象数组,而不能只是id字符串数组 articleIds = [...new Set(articleIds)].filter(id => isObjectId(id)).map(id => createObjectId(id)) - const counts = await CommentModel.aggregate([ + const counts = await commentProxy.aggregate([ { $match: { state: 1, article: { $in: articleIds } } }, { $group: { _id: '$article', total_count: { $sum: 1 } } } ]) @@ -545,14 +534,11 @@ async function updateArticleCommentCount (articleIds = []) { debug.error('更新文章评论数量前聚合评论数据操作失败,错误:', err.message) return [] }) - Promise.all(counts.map(count => ArticleModel.update( - { _id: count._id }, - { $set: { 'meta.comments': count.total_count } } - ).exec().catch(err => { - debug.error('文章评论数量更新失败,错误:', err.message) - }))).then(() => { - debug.success('文章评论数量更新成功') - }) + Promise.all( + counts.map(count => articleProxy.updateById(count._id, { $set: { 'meta.comments': count.total_count } }).exec()) + ) + .then(() => debug.success('文章评论数量更新成功')) + .catch(err => debug.error('文章评论数量更新失败,错误:', err.message)) } // 发送邮件 @@ -562,7 +548,7 @@ async function sendEmailToAdminAndUser (comment, permalink) { let adminType = '评论' if (type === 0) { // 文章评论 - const at = await ArticleModel.findById(article).exec() + const at = await articleProxy.getById(article).exec().catch(() => null) if (at && at._id) { adminTitle = `博客文章 [${at.title}] 有了新的评论` } @@ -582,7 +568,7 @@ async function sendEmailToAdminAndUser (comment, permalink) { // 发送给被评论者 if (comment.forward) { - const forwardAuthor = await UserModel.findById(comment.forward.author).exec().catch(() => null) + const forwardAuthor = await userProxy.getById(comment.forward.author).exec().catch(() => null) if (forwardAuthor) { mailer.send({ to: forwardAuthor.github.email, @@ -615,29 +601,26 @@ async function checkAuthor (author) { if (author.id) { // 更新 if (isObjectId(author.id)) { - user = await UserModel.findByIdAndUpdate(author.id, update, { - new: true - }).exec().catch(err => { - debug.error('用户更新失败,错误:', err.message) - this.log.error(err.message) - return null - }) + user = await userProxy.updateByIdAndNotify(author.id, update) + .catch(err => { + debug.error('用户更新失败,错误:', err.message) + this.log.error(err.message) + return null + }) if (user) { debug.success(`用户【${user.name}】更新成功`) } } } else { // 创建 - user = await new UserModel({ + user = await userProxy.createAndNotify({ ...update, role: config.constant.roleMap.USER + }).catch(err => { + debug.error('用户创建失败,错误:', err.message) + this.log.error(err.message) + return null }) - .save() - .catch(err => { - debug.error('用户创建失败,错误:', err.message) - this.log.error(err.message) - return null - }) if (user) { debug.success(`用户【${user.name}】创建成功`) } @@ -647,7 +630,7 @@ async function checkAuthor (author) { } async function findUser (query = {}, update) { - const user = await UserModel.findOne(query).select('-password').exec().catch(err => { + const user = await userProxy.findOne(query).select('-password').exec().catch(err => { debug.error('用户查找失败,错误:', err.message) return null }) diff --git a/server/controller/index.js b/server/controller/index.js index c0ce76e..6420112 100644 --- a/server/controller/index.js +++ b/server/controller/index.js @@ -17,3 +17,4 @@ exports.auth = require('./auth') exports.moment = require('./moment') exports.statistics = require('./statistics') exports.aliyun = require('./aliyun') +exports.notification = require('./notification') diff --git a/server/controller/moment.js b/server/controller/moment.js index e707988..42bc531 100644 --- a/server/controller/moment.js +++ b/server/controller/moment.js @@ -7,9 +7,10 @@ 'use strict' const config = require('../config') -const { MomentModel } = require('../model') -const { getLocation } = require('../util') +const { momentProxy } = require('../proxy') +const { getLocation, getDocsPaginationData } = require('../util') +// 动态列表 exports.list = async (ctx, next) => { const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.momentLimit).toInt().gt(0, '每页数量必须大于0').val() const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() @@ -38,29 +39,17 @@ exports.list = async (ctx, next) => { } } - const moments = await MomentModel.paginate(query, options).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (moments) { - ctx.success({ - list: moments.docs, - pagination: { - total: moments.total, - current_page: moments.page > moments.pages ? moments.pages : moments.page, - total_page: moments.pages, - per_page: moments.limit - } - }) - } else { - ctx.fail(-1) - } + const moments = await momentProxy.paginate(query, options) + + moments + ? ctx.success(getDocsPaginationData(moments), '动态列表获取成功') + : ctx.fail('动态列表获取失败') } +// 创建动态 exports.create = async (ctx, next) => { const content = ctx.validateBody('content').required('缺少内容').notEmpty().val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], '个人动态状态参数无效').val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数无效').val() const req = ctx.req const moment = {} const { ip, location } = getLocation(req) @@ -71,53 +60,37 @@ exports.create = async (ctx, next) => { moment.location = { ip, ...location } moment.content = content - const data = await new MomentModel(moment).save().catch(err => { - ctx.log.error(err.message) - return null - }) + const data = await momentProxy.newAndSave(moment) - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + data && data.length + ? ctx.success(data, '动态创建成功') + : ctx.fail('动态创建失败') } +// 动态更新 exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('个人动态ID参数无效').toString().isObjectId('个人动态ID参数无效').val() - const content = ctx.validateBody('content').optional().isString('内容参数必须是字符串类型').val() - const state = ctx.validateBody('state').optional().toInt().isIn([-2, 0, 1, 2], '个人动态状态参数无效').val() + const id = ctx.validateParam('id').required('缺少动态ID').toString().isObjectId().val() + const content = ctx.validateBody('content').optional().toString().val() + const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数无效').val() const moment = {} if (state !== undefined) { moment.state = state } content && (moment.content = content) + const data = await momentProxy.updateById(id, moment).exec() - const data = await MomentModel.findByIdAndUpdate(id, moment, { - new: true - }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data) { - ctx.success(data) - } else { - ctx.fail() - } + data + ? ctx.success(data, '动态更新成功') + : ctx.fail('动态更新失败') } +// 删除动态 exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('个人动态ID参数无效').toString().isObjectId('个人动态ID参数无效').val() - const data = await MomentModel.remove({ _id: id }).catch(err => { - ctx.log.error(err.message) - return null - }) - - if (data && data.result && data.result.ok) { - ctx.success() - } else { - ctx.fail() - } + const id = ctx.validateParam('id').required('缺少动态ID').toString().isObjectId().val() + const data = await momentProxy.delById(id).exec() + + data && data.result && data.result.ok + ? ctx.success(null, '动态删除成功') + : ctx.fail('动态删除失败') } diff --git a/server/controller/notification.js b/server/controller/notification.js new file mode 100644 index 0000000..15df8b9 --- /dev/null +++ b/server/controller/notification.js @@ -0,0 +1,83 @@ +/** + * @desc Notification controller + * @author Jooger + * @date 12 Feb 2018 + */ + +'use strict' + +const config = require('../config') +const { notificationProxy } = require('../proxy') +const { getDocsPaginationData } = require('../util') +const { typeMap, categoryMap } = config.constant.notification + +// 通知列表 +exports.list = async (ctx, next) => { + const pageSize = ctx.validateQuery('per_page').defaultTo(20).toInt().gt(0, 'per_page参数必须大于0').val() + const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'page参数必须大于0').val() + const type = ctx.validateQuery('type').optional().toInt().isIn(Object.values(typeMap), 'type参数错误').val() + const category = ctx.validateQuery('category').optional().toInt().isIn(Object.values(categoryMap), 'category参数错误').val() + const viewed = ctx.validateQuery('viewed').optional().toString().val() + + // 过滤条件 + const options = { + sort: { + createdAt: -1 + }, + page, + limit: pageSize, + populate: [ + { + path: 'user', + select: 'name email site' + }, + { + path: 'article', + select: 'title permalink' + } + ] + } + + // 查询条件 + const query = {} + + if (type !== undefined) { + query.type = type + } + + if (category !== undefined) { + query.category = category + } + + if (viewed !== undefined) { + query.viewed = viewed + } + + const ns = await notificationProxy.paginate(query, options) + + ns + ? ctx.success(getDocsPaginationData(ns), '通知列表获取成功') + : ctx.fail('通知列表获取失败') +} + +// 通知已读 +exports.view = async (ctx, next) => { + const id = ctx.validateParam('id').required('缺少通知ID').toString().isObjectId().val() + const data = await notificationProxy.updateById(id, { + viewed: true + }).exec() + + data + ? ctx.success(null, '通知标记已读成功') + : ctx.fail('通知标记已读失败') +} + +// 删除通知 +exports.delete = async (ctx, next) => { + const id = ctx.validateParam('id').required('缺少通知ID').toString().isObjectId().val() + const data = await notificationProxy.delById(id).exec() + + data && data.result && data.result.ok + ? ctx.success(null, '通知删除成功') + : ctx.fail('通知删除失败') +} diff --git a/server/controller/statistics.js b/server/controller/statistics.js index 7acc3aa..eaa4af9 100644 --- a/server/controller/statistics.js +++ b/server/controller/statistics.js @@ -6,7 +6,7 @@ const { articleProxy, categoryProxy, tagProxy, userProxy, commentProxy } = require('../proxy') -// TODO: 站内统计 +// OPTIMIZE: 站内统计 exports.data = async (ctx, next) => { const data = await Promise.all([ articleProxy.count({ state: 1 }).exec(), diff --git a/server/controller/user.js b/server/controller/user.js index d4a9026..349801e 100644 --- a/server/controller/user.js +++ b/server/controller/user.js @@ -93,6 +93,10 @@ exports.password = async (ctx, next) => { exports.mute = async (ctx, next) => { const id = ctx.validateParam('id').required('缺少用户ID').toString().isObjectId().val() const mute = ctx.validateBody('mute').defaultTo(true).toBoolean().val() + const user = userProxy.getById(id).exec() + if (user && !user.role) { + return ctx.fail('管理员不能禁言') + } const data = await userProxy.updateById(id, { mute }).exec() const msg = mute ? '用户禁言' : '用户解禁' data diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index 06226b7..29a4247 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -8,11 +8,9 @@ const compose = require('koa-compose') const jwt = require('jsonwebtoken') -// const passport = require('koa-passport') const config = require('../config') const { userProxy } = require('../proxy') const debug = require('../util').getDebug('Auth') -// const isProd = process.env.NODE_ENV === 'production' const redirectReg = /auth\/github\/login(.*?)/ // 验证本地登录token @@ -89,62 +87,3 @@ exports.isAuthenticated = () => { } ]) } - -// 第三方登录验证 -// exports.isSnsAuthenticated = () => { -// return compose( -// Object.keys(config.sns).map(name => { -// return vertifySnsToken(name) -// }).concat([ -// async (ctx, next) => { -// if (!ctx.session._snsVerify) { -// return ctx.fail(401) -// } - -// const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) -// const user = await UserModel.findById(userId).exec().catch(err => { -// debug.error('用户查找失败, 错误:', err.message) -// ctx.log.error(err.message) -// return null -// }) -// if (!user) { -// return ctx.fail(401, '用户不存在') -// } -// ctx._user = user.toObject() -// ctx._isSnsAuthenticated = true -// await next() -// } -// ])) -// } - -// 单个第三方登录验证 -// exports.snsAuth = (name = '') => { -// return compose([ -// vertifySnsToken(name), -// async (ctx, next) => { -// // 如果已经登录 -// const redirectUrl = ctx.query.redirectUrl || config.site -// if (ctx.session._snsVerify) { -// debug.info('您已经登录, 重定向中...') -// return ctx.redirect(redirectUrl) -// } -// ctx.session.passport = { redirectUrl } -// await next() -// }, -// passport.authenticate(name, { -// failureRedirect: '/', -// session: false -// }) -// ]) -// } - -// 单个第三方登录退出 -// exports.snsLogout = (name = '') => compose([ -// vertifySnsToken(name), -// async (ctx, next) => { -// if (!ctx.session._snsVerify) { -// return ctx.fail(-1, '请您先登录') -// } -// await next() -// } -// ]) diff --git a/server/model/schema/article.js b/server/model/schema/article.js index d057890..36c06ee 100644 --- a/server/model/schema/article.js +++ b/server/model/schema/article.js @@ -38,9 +38,9 @@ const articleSchema = new mongoose.Schema({ publishedAt: { type: Date, default: Date.now }, // 文章元数据 (浏览量, 喜欢数, 评论数) meta: { - pvs: { type: Number, default: 0 }, - ups: { type: Number, default: 0 }, - comments: { type: Number, default: 0 } + pvs: { type: Number, default: 0, validate: /^\d*$/ }, + ups: { type: Number, default: 0, validate: /^\d*$/ }, + comments: { type: Number, default: 0, validate: /^\d*$/ } } }) diff --git a/server/model/schema/comment.js b/server/model/schema/comment.js index 22bb0fc..a8ec26d 100644 --- a/server/model/schema/comment.js +++ b/server/model/schema/comment.js @@ -18,7 +18,7 @@ const commentSchema = new mongoose.Schema({ state: { type: Number, default: 1 }, // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 spam: { type: Boolean, default: false }, // Akismet判定是否是垃圾评论,方便后台check author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, - ups: { type: Number, default: 0 }, // 点赞数 + ups: { type: Number, default: 0, validate: /^\d*$/ }, // 点赞数 sticky: { type: Number, default: 0 }, // 是否置顶 0 否 | 1 是 type: { type: Number, default: 0 }, // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) meta: { diff --git a/server/model/schema/index.js b/server/model/schema/index.js index f81642a..1d50d2e 100644 --- a/server/model/schema/index.js +++ b/server/model/schema/index.js @@ -13,4 +13,4 @@ exports.tag = require('./tag') exports.user = require('./user') exports.option = require('./option') exports.moment = require('./moment') -exports.log = require('./log') +exports.notification = require('./notification') diff --git a/server/model/schema/log.js b/server/model/schema/log.js deleted file mode 100644 index b611717..0000000 --- a/server/model/schema/log.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @desc Site Log - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate') - -const logSchema = new mongoose.Schema({ - createdAt: { type: Date, default: Date.now } -}) - -logSchema.plugin(mongoosePaginate) - -module.exports = logSchema diff --git a/server/model/schema/notification.js b/server/model/schema/notification.js new file mode 100644 index 0000000..90d7a92 --- /dev/null +++ b/server/model/schema/notification.js @@ -0,0 +1,39 @@ +/** + * @desc Notification + * @author Jooger + * @date 12 Feb 2018 + */ + +'use strict' + +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate') +const config = require('../../config') + +const notificationSchema = new mongoose.Schema({ + // 通知类型 0 系统通知 | 1 评论通知 | 2 点赞通知 | 3 用户操作通知 + type: { type: Number, required: true, validate: typeValidator }, + // 类型细化分类 + category: { type: String, required: true, validate: categoryValidator }, + // 是否已读 + viewed: { type: Boolean, default: false }, + // article user comment 根据情况是否包含 + article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + comment: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, + // 必填 + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now } +}) + +function typeValidator (val) { + return Object.values(config.constant.notification.typeMap).includes(+val) +} + +function categoryValidator (val) { + return Object.values(config.constant.notification.categoryMap).includes(val + '') +} + +notificationSchema.plugin(mongoosePaginate) + +module.exports = notificationSchema diff --git a/server/plugins/mailer.js b/server/plugins/mailer.js index a7ac490..32439da 100644 --- a/server/plugins/mailer.js +++ b/server/plugins/mailer.js @@ -49,7 +49,7 @@ exports.start = async () => { */ exports.send = (opt = {}, toMe = false) => { if (!isVerify) { - return debug.error('客户端未验证,拒绝发送邮件') + return debug.warn('客户端未验证,拒绝发送邮件') } opt.from = `${config.author} <${config.email}>` if (toMe) { diff --git a/server/proxy/article.js b/server/proxy/article.js index d681940..e8be41f 100644 --- a/server/proxy/article.js +++ b/server/proxy/article.js @@ -8,11 +8,35 @@ const BaseProxy = require('./base') const { ArticleModel } = require('../model') +const notificationProxy = require('./notification') +const config = require('../config') +const { typeMap, categoryMap } = config.constant.notification class ArticleProxy extends BaseProxy { constructor () { super(ArticleModel) } + + likeAndNotify (id, like, user) { + return this.updateById(id, { + $inc: { + 'meta.ups': like ? 1 : -1 + } + }).exec().then(res => { + if (res) { + const payload = { + type: typeMap.LIKE, + category: categoryMap[like ? 'LIKE_ARTICLE' : 'UNLIKE_ARTICLE'], + article: id + } + if (user) { + payload.user = typeof user === 'string' ? user : user._id + } + notificationProxy.gen(payload) + } + return res + }) + } } module.exports = new ArticleProxy() diff --git a/server/proxy/comment.js b/server/proxy/comment.js index 7e0f6c4..5685463 100644 --- a/server/proxy/comment.js +++ b/server/proxy/comment.js @@ -8,11 +8,49 @@ const BaseProxy = require('./base') const { CommentModel } = require('../model') +const notificationProxy = require('./notification') +const config = require('../config') +const { typeMap, categoryMap } = config.constant.notification class CommentProxy extends BaseProxy { constructor () { super(CommentModel) } + + // 生成通知的创建 + createAndNotify (model) { + return this.newAndSave(model).then(res => { + if (res && res.length) { + notificationProxy.gen({ + type: typeMap.COMMENT, + category: categoryMap[model.forward ? 'COMMENT_REPLY' : 'COMMENT_COMMENT'], + comment: res[0]._id + }) + } + return res[0] + }) + } + + likeAndNotify (id, like, user) { + return this.updateById(id, { + $inc: { + ups: like ? 1 : -1 + } + }).exec().then(res => { + if (res) { + const payload = { + type: typeMap.LIKE, + category: categoryMap[like ? 'LIKE_COMMENT' : 'UNLIKE_COMMENT'], + comment: id + } + if (user) { + payload.user = typeof user === 'string' ? user : user._id + } + notificationProxy.gen(payload) + } + return res + }) + } } module.exports = new CommentProxy() diff --git a/server/proxy/index.js b/server/proxy/index.js index 8b4680c..4b8a830 100644 --- a/server/proxy/index.js +++ b/server/proxy/index.js @@ -13,5 +13,6 @@ module.exports = { userProxy: require('./user'), commentProxy: require('./comment'), optionProxy: require('./option'), - momentProxy: require('./moment') + momentProxy: require('./moment'), + notificationProxy: require('./notification') } diff --git a/server/proxy/notification.js b/server/proxy/notification.js new file mode 100644 index 0000000..46dca8f --- /dev/null +++ b/server/proxy/notification.js @@ -0,0 +1,39 @@ +/** + * @desc Notification model proxy + * @author Jooger + * @date 26 Jan 2018 + */ + +'use strict' + +const BaseProxy = require('./base') +const { NotificationModel } = require('../model') +const config = require('../config') +const { getDebug } = require('../util') +const debug = getDebug('Notification') +const { typeMap, categoryMap } = config.constant.notification + +class NotificationProxy extends BaseProxy { + constructor () { + super(NotificationModel) + } + + // 生成站内消息 + gen (model) { + return this.newAndSave(model).then(res => { + if (res && res.length) { + debug('通知生成成功,', `类型[${getKeyByValue(typeMap, model.type)}],分类[${getKeyByValue(categoryMap, model.category)}],ID[${res[0]._id}]`) + } + return res + }).catch(err => { + debug.error('通知生成失败,错误:', err.message) + return null + }) + } +} + +function getKeyByValue (obj, val) { + return Object.keys(obj).find(key => val === obj[key]) +} + +module.exports = new NotificationProxy() diff --git a/server/proxy/user.js b/server/proxy/user.js index be4c54e..9d21f32 100644 --- a/server/proxy/user.js +++ b/server/proxy/user.js @@ -8,11 +8,57 @@ const BaseProxy = require('./base') const { UserModel } = require('../model') +const notificationProxy = require('./notification') +const config = require('../config') +const { typeMap, categoryMap } = config.constant.notification class UserProxy extends BaseProxy { constructor () { super(UserModel) } + + // 生成通知的创建 + createAndNotify (model) { + return this.newAndSave(model).then(res => { + if (res && res.length && !!res[0].role) { + // 管理员不生成通知 + notify(typeMap.USER, categoryMap.USER_CREATE, res[0]._id) + } + return res[0] + }) + } + + // 生成通知的更新 + updateByIdAndNotify (id, doc, opt = {}) { + return this.updateById(...arguments).exec().then(res => { + if (res && !!res.role) { + // 管理员不生成通知 + notify(typeMap.USER, categoryMap.USER_UPDATE, id) + } + return res + }) + } + + muteByIdAndNotify (id) { + return this.updateById(id, { mute: true }).exec().then(res => { + if (res && !!res.role) { + // 管理员不生成通知 + notify(typeMap.GENERAL, categoryMap.USER_UPDATE, id) + } + return res + }) + } +} + +function notify (type, category, user) { + if (!type || !category || !user) { + return + } + notificationProxy.gen({ + type, + category, + user + }) } module.exports = new UserProxy() diff --git a/server/routes/backend.js b/server/routes/backend.js index 706344f..be2b676 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -18,7 +18,8 @@ const { music, statistics, moment, - aliyun + aliyun, + notification } = require('../controller') const { authenticate } = require('../middleware') const isAuthenticated = authenticate.isAuthenticated() @@ -84,9 +85,14 @@ router.patch('/moments/:id', isAuthenticated, moment.update) router.delete('/moments/:id', isAuthenticated, moment.delete) // Statistics -// OPTIMIZE: router.get('/statistics', isAuthenticated, statistics.data) +// Aliyun OSS router.get('/aliyun/oss', isAuthenticated, aliyun.oss) +// Notifications +router.get('/notifications', isAuthenticated, notification.list) +router.post('/notifications/:id/view', isAuthenticated, notification.view) +router.delete('/notifications/:id', isAuthenticated, notification.delete) + module.exports = router diff --git a/server/service/github-passport.js b/server/service/github-passport.js deleted file mode 100644 index e80f2d1..0000000 --- a/server/service/github-passport.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @desc github password service - * @author Jooger - * @date 27 Sep 2017 - */ - -'use strict' - -const passport = require('koa-passport') -const GithubStrategy = require('passport-github').Strategy -const config = require('../config') -const { clientID, clientSecret, callbackURL } = config.sns.github -const { randomString, getDebug, proxy } = require('../util') -const debug = getDebug('Github:Auth') - -exports.init = (UserModel, config) => { - passport.use(new GithubStrategy({ - clientID, - clientSecret, - callbackURL, - passReqToCallback: true - }, async (req, accessToken, refreshToken, profile, done) => { - debug('Github权限验证开始...') - try { - const user = await UserModel.findOne({ - 'github.id': profile.id - }).catch(err => { - debug.error('本地用户查找失败, 错误:', err.message) - return null - }) - - if (user) { - const userData = { - name: profile.displayName || profile.username, - avatar: proxy(profile._json.avatar_url), - slogan: profile._json.bio, - github: profile._json, - role: user.role - } - // userData.github.token = accessToken - const updatedUser = await UserModel.findByIdAndUpdate(user._id, userData).exec().catch(err => { - debug.error('本地用户更新失败, 错误:', err.message) - }) || user - - return end(null, { - ...updatedUser.toObject(), - token: accessToken - }) - } - - const newUser = { - name: profile.displayName || profile.username, - avatar: proxy(profile._json.avatar_url), - slogan: profile._json.bio, - github: profile._json, - role: 1 - } - - // newUser.github.token = accessToken - - const checkUser = await UserModel.findOne({ name: newUser.name }).exec().catch(err => { - debug.error('本地用户查找失败, 错误:', err.message) - return true - }) - - if (checkUser) { - newUser.name += '-' + randomString() - } - - const data = await new UserModel(newUser).save().catch(err => { - debug.error('本地用户创建失败, 错误:', err.message) - }) - - return end(null, { - ...data.toObject(), - token: accessToken - }) - } catch (err) { - debug.error('Github权限验证失败,错误:', err) - return end(err) - } - - function end (err, data) { - debug.success('Github权限验证成功') - done(err, data) - } - })) -} diff --git a/server/service/index.js b/server/service/index.js index ca5cd85..da91a75 100644 --- a/server/service/index.js +++ b/server/service/index.js @@ -8,7 +8,6 @@ const { getGithubUsersInfo, getGithubAuthUserInfo } = require('./github-userinfo') -// exports.githubPassport = require('./github-passport') exports.getGithubUsersInfo = getGithubUsersInfo exports.getGithubAuthUserInfo = getGithubAuthUserInfo exports.getGithubToken = require('./github-token') From 38466e330e98661fa1598aa678f955cda0f0adb1 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Fri, 23 Feb 2018 10:26:45 +0800 Subject: [PATCH 101/208] [update] update notification api --- server/controller/notification.js | 19 ++++++++++++++++++- server/middleware/authenticate.js | 21 --------------------- server/routes/backend.js | 2 ++ server/routes/index.js | 2 -- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/server/controller/notification.js b/server/controller/notification.js index 15df8b9..f2d9904 100644 --- a/server/controller/notification.js +++ b/server/controller/notification.js @@ -50,7 +50,7 @@ exports.list = async (ctx, next) => { } if (viewed !== undefined) { - query.viewed = viewed + query.viewed = viewed === 'true' } const ns = await notificationProxy.paginate(query, options) @@ -60,6 +60,15 @@ exports.list = async (ctx, next) => { : ctx.fail('通知列表获取失败') } +// 未读通知数量 +exports.count = async (ctx, next) => { + const data = await notificationProxy.count({ viewed: false }).exec() + + data + ? ctx.success(data, '未读通知数量获取成功') + : ctx.fail('未读通知数量获取失败') +} + // 通知已读 exports.view = async (ctx, next) => { const id = ctx.validateParam('id').required('缺少通知ID').toString().isObjectId().val() @@ -72,6 +81,14 @@ exports.view = async (ctx, next) => { : ctx.fail('通知标记已读失败') } +// 通知已读 +exports.viewAll = async (ctx, next) => { + const data = await notificationProxy.updateMany({ viewed: false }, { viewed: true }).exec() + data && data.ok + ? ctx.success(null, '通知全部标记已读成功') + : ctx.fail('通知全部标记已读失败') +} + // 删除通知 exports.delete = async (ctx, next) => { const id = ctx.validateParam('id').required('缺少通知ID').toString().isObjectId().val() diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js index 29a4247..ab94783 100644 --- a/server/middleware/authenticate.js +++ b/server/middleware/authenticate.js @@ -42,27 +42,6 @@ function verifyToken () { } } -// 验证第三方登录token -// function vertifySnsToken (name = '') { -// return async (ctx, next) => { -// if (ctx.session._snsVerify) { -// await next() -// } -// ctx.session._snsVerify = false -// if (config.sns[name]) { -// const token = ctx.cookies.get(config.sns[name].key, { signed: false }) - -// if (token) { -// ctx.session._snsVerify = true -// ctx.session._snsToken = token -// ctx.session._snsName = name -// debug.success('【%s】第三方登录Token校验成功', name) -// } -// } -// await next() -// } -// } - // 本地登录验证 exports.isAuthenticated = () => { return compose([ diff --git a/server/routes/backend.js b/server/routes/backend.js index be2b676..9974fbe 100644 --- a/server/routes/backend.js +++ b/server/routes/backend.js @@ -92,7 +92,9 @@ router.get('/aliyun/oss', isAuthenticated, aliyun.oss) // Notifications router.get('/notifications', isAuthenticated, notification.list) +router.get('/notifications/count', isAuthenticated, notification.count) router.post('/notifications/:id/view', isAuthenticated, notification.view) +router.post('/notifications/viewall', isAuthenticated, notification.viewAll) router.delete('/notifications/:id', isAuthenticated, notification.delete) module.exports = router diff --git a/server/routes/index.js b/server/routes/index.js index 7734c2a..b0f6c2a 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -16,7 +16,6 @@ module.exports = app => { router.use('*', header) router.get('/', async (ctx, next) => { - ctx.log.info('Got a root request from %s for %s', ctx.request.ip, ctx.path) ctx.body = { name: config.name, version: config.version, @@ -32,7 +31,6 @@ module.exports = app => { router.all('*', (ctx, next) => { ctx.fail(404, `${ctx.path} 不支持 ${ctx.method} 请求类型`) - ctx.status = 404 }) app.use(router.routes(), router.allowedMethods()) From 92e43a65de19d66455fe067e0415f72627a34f89 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Fri, 23 Feb 2018 11:17:19 +0800 Subject: [PATCH 102/208] [update] update simple-netease-cloud-music --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fbf9662..fe7329c 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "nodemailer": "^4.4.1", "passport-github": "^1.1.0", "redis": "^2.8.0", - "simple-netease-cloud-music": "^0.3.0", + "simple-netease-cloud-music": "^0.3.4", "validator": "^9.2.0" }, "devDependencies": { From 33be0dfa0ea0af9cb777506e1509b2e8f62d095f Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Fri, 23 Feb 2018 19:57:06 +0800 Subject: [PATCH 103/208] [update] update option schema --- server/controller/category.js | 2 +- server/controller/tag.js | 4 ++-- server/model/schema/option.js | 30 ++++++------------------------ 3 files changed, 9 insertions(+), 27 deletions(-) diff --git a/server/controller/category.js b/server/controller/category.js index 5ed60d8..9de64b9 100644 --- a/server/controller/category.js +++ b/server/controller/category.js @@ -91,7 +91,7 @@ exports.create = async (ctx, next) => { }) data && data.length - ? ctx.success(data, '分类创建成功') + ? ctx.success(data[0], '分类创建成功') : ctx.fail('分类创建失败') } else { ctx.fail(`【${name}】分类已经存在`) diff --git a/server/controller/tag.js b/server/controller/tag.js index 8b24e25..8022677 100644 --- a/server/controller/tag.js +++ b/server/controller/tag.js @@ -28,7 +28,7 @@ exports.list = async (ctx, next) => { if (typeof data[i].toObject === 'function') { data[i] = data[i].toObject() } - const articles = await articleProxy.find({ tag: data[i]._id }).exec().catch(err => { + const articles = await articleProxy.find({ tag: data[i]._id, state: 1 }).exec().catch(err => { ctx.log.error(err.message) return [] }) @@ -82,7 +82,7 @@ exports.create = async (ctx, next) => { }) data && data.length - ? ctx.success(data, '标签创建成功') + ? ctx.success(data[0], '标签创建成功') : ctx.fail('标签创建失败') } else { ctx.fail(`【${name}】标签已经存在`) diff --git a/server/model/schema/option.js b/server/model/schema/option.js index 8703a22..eb36505 100644 --- a/server/model/schema/option.js +++ b/server/model/schema/option.js @@ -9,31 +9,13 @@ const mongoose = require('mongoose') const optionSchema = new mongoose.Schema({ - title: { type: String, default: '' }, - subtitle: { type: String, default: '' }, welcome: { type: String, default: '' }, - description: [{ type: String, default: '' }], - banners: [{ type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }], - errorBanner: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, - hobby: [{ - name: { type: String, required: true }, - icon: { type: String, required: true } - }], - experience: [{ - time: { type: String, required: true }, - title: { type: String, required: true }, - subtitle: { type: String, default: '' } - }], - skill: [{ - title: { type: String, required: true }, - level: { type: String, required: true }, - icon: { type: String, required: true } - }], - contact: [{ - title: { type: String, required: true }, - url: { type: String, required: true }, - icon: { type: String, required: true } - }], + description: { type: String, default: '' }, + hobby: { type: String, default: '' }, + skill: { type: String, default: '' }, + music: { type: String, default: '' }, + location: { type: String, default: '' }, + company: { type: String, default: '' }, links: [{ name: { type: String, required: true }, github: { type: String, default: '' }, From 9cd7875850b4904b95a37a72af97d38d151bbb58 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Sat, 24 Feb 2018 15:42:40 +0800 Subject: [PATCH 104/208] [update] update mongo plugin --- server/plugins/mongo.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/server/plugins/mongo.js b/server/plugins/mongo.js index 0b923f0..3a4e6ef 100644 --- a/server/plugins/mongo.js +++ b/server/plugins/mongo.js @@ -9,7 +9,7 @@ const config = require('../config') const mongoose = require('mongoose') const { bhash, getDebug, proxy } = require('../util') -const { UserModel, OptionModel } = require('../model') +const { userProxy, optionProxy } = require('../proxy') const { getGithubUsersInfo } = require('../service') const debug = getDebug('MongoDB') let isConnected = false @@ -38,16 +38,16 @@ function seed () { // 参数初始化 async function seedOption () { - const option = await OptionModel.findOne().exec().catch(err => debug.error(err.message)) + const option = await optionProxy.findOne().exec().catch(err => debug.error(err.message)) if (!option) { - await new OptionModel().save().catch(err => debug.error(err.message)) + await optionProxy.newAndSave().catch(err => debug.error(err.message)) } } // 管理员初始化 async function seedAdmin () { - const admin = await UserModel.findOne({ + const admin = await userProxy.findOne({ role: config.constant.roleMap.ADMIN, 'github.login': config.author }).exec() @@ -63,7 +63,7 @@ async function createAdmin () { return fail('未找到Github用户数据') } data = data[0] - const result = await new UserModel({ + const result = await userProxy.newAndSave({ role: config.constant.roleMap.ADMIN, name: data.name, email: data.email, @@ -77,11 +77,9 @@ async function createAdmin () { id: data.id, login: data.login } - }) - .save() - .catch(err => fail(err.message)) + }).catch(err => fail(err.message)) - if (!result || !result._id) { + if (!result || !result.length) { fail('本地入库失败') } else { debug.success('初始化管理员成功') From 62386a266b23c7719e6ceacc0237ae5b8e5922d9 Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Sat, 24 Feb 2018 16:12:13 +0800 Subject: [PATCH 105/208] [update] add 163 email config --- server/config/index.js | 1 + server/plugins/mailer.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/config/index.js b/server/config/index.js index 23e7378..643027f 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -16,6 +16,7 @@ const baseConfig = { author: packageInfo.author.name, site: packageInfo.author.url, email: packageInfo.author.email, + email_163: 'zzy1198258955@163.com', env: process.env.NODE_ENV, root: path.resolve(__dirname, '../../'), port: process.env.PORT || 3001, diff --git a/server/plugins/mailer.js b/server/plugins/mailer.js index 32439da..26404e7 100644 --- a/server/plugins/mailer.js +++ b/server/plugins/mailer.js @@ -17,7 +17,7 @@ const transporter = isProd ? nodemailer.createTransport({ service: '163', secure: true, auth: { - user: config.email, + user: config.email_163, pass: process.env['163Pass'] || '163邮箱密码' } }) : null From 02a2f858cb5f895e21e8d0128c80e3019d169b1e Mon Sep 17 00:00:00 2001 From: zhuzhiyang Date: Sat, 24 Feb 2018 17:50:49 +0800 Subject: [PATCH 106/208] [fix] fix category and tag api does not update eextends --- server/controller/category.js | 2 ++ server/controller/tag.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/server/controller/category.js b/server/controller/category.js index 9de64b9..fdab5e7 100644 --- a/server/controller/category.js +++ b/server/controller/category.js @@ -104,11 +104,13 @@ exports.update = async (ctx, next) => { const name = ctx.validateBody('name').optional().val() const description = ctx.validateBody('description').optional().val() const list = ctx.validateBody('list').optional().toInt().val() + const ext = ctx.validateBody('extends').optional().toArray().val() const category = {} name && (category.name = name) description && (category.description = description) list && (category.list = list) + ext && (category.extends = ext) const data = await categoryProxy.updateById(id, category).exec() diff --git a/server/controller/tag.js b/server/controller/tag.js index 8022677..2383cc7 100644 --- a/server/controller/tag.js +++ b/server/controller/tag.js @@ -94,10 +94,12 @@ exports.update = async (ctx, next) => { const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() const name = ctx.validateBody('name').optional().val() const description = ctx.validateBody('description').optional().val() + const ext = ctx.validateBody('extends').optional().toArray().val() const tag = {} name && (tag.name = name) description && (tag.description = description) + ext && (tag.extends = ext) const data = await tagProxy.updateById(id, tag).exec() From a2db2db12736502a43b8a9ef53f9818e51e66924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 26 Jun 2018 17:04:27 +0800 Subject: [PATCH 107/208] =?UTF-8?q?update:=20=E6=96=87=E7=AB=A0=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=E5=8F=98=E6=9B=B4=E6=9C=AA=E6=9B=B4=E6=96=B0=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E4=BC=98=E5=85=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/controller/article.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/controller/article.js b/server/controller/article.js index cd971f2..e04d0d5 100644 --- a/server/controller/article.js +++ b/server/controller/article.js @@ -36,6 +36,7 @@ exports.list = async (ctx, next) => { // 过滤条件 const options = { sort: { + updatedAt: -1, createdAt: -1 }, page, From d24cd4ed5e6163118f499054b9b94502cbc733cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 20 Aug 2018 20:08:17 +0800 Subject: [PATCH 108/208] update: egg init --- . dockerignore | 7 - .autod.conf.js | 30 ++ .editorconfig | 9 + .eslintignore | 3 +- .eslintrc | 6 + .eslintrc.js | 24 - .gitignore | 36 +- .travis.yml | 10 + Dockerfile | 20 - LICENSE | 21 - README.md | 128 +----- README.zh-CN.md | 39 ++ api.md | 74 --- app/controller/home.js | 11 + logs/.gitkeep => app/model/article.js | 0 app/router.js | 9 + appveyor.yml | 14 + bin/www | 90 ---- config/config.default.js | 15 + config/plugin.js | 9 + docker-start.sh | 12 - package.json | 119 ++--- server/app.js | 75 --- server/config/development.js | 21 - server/config/index.js | 134 ------ server/config/production.js | 28 -- server/config/test.js | 9 - server/controller/aliyun.js | 21 - server/controller/article.js | 451 ------------------ server/controller/auth.js | 155 ------- server/controller/category.js | 136 ------ server/controller/comment.js | 638 -------------------------- server/controller/index.js | 20 - server/controller/moment.js | 96 ---- server/controller/music.js | 88 ---- server/controller/notification.js | 100 ---- server/controller/option.js | 27 -- server/controller/statistics.js | 31 -- server/controller/tag.js | 125 ----- server/controller/user.js | 159 ------- server/middleware/authenticate.js | 68 --- server/middleware/error.js | 30 -- server/middleware/formidable.js | 39 -- server/middleware/header.js | 32 -- server/middleware/index.js | 13 - server/middleware/response.js | 41 -- server/model/index.js | 39 -- server/model/schema/article.js | 49 -- server/model/schema/category.js | 24 - server/model/schema/comment.js | 39 -- server/model/schema/index.js | 16 - server/model/schema/moment.js | 22 - server/model/schema/notification.js | 39 -- server/model/schema/option.js | 29 -- server/model/schema/tag.js | 22 - server/model/schema/user.js | 37 -- server/plugins/akismet.js | 150 ------ server/plugins/crontab.js | 21 - server/plugins/index.js | 14 - server/plugins/mailer.js | 64 --- server/plugins/mongo.js | 91 ---- server/plugins/redis.js | 74 --- server/plugins/validation.js | 45 -- server/proxy/article.js | 42 -- server/proxy/base.js | 81 ---- server/proxy/category.js | 18 - server/proxy/comment.js | 56 --- server/proxy/index.js | 18 - server/proxy/moment.js | 18 - server/proxy/notification.js | 39 -- server/proxy/option.js | 18 - server/proxy/tag.js | 18 - server/proxy/user.js | 64 --- server/routes/backend.js | 100 ---- server/routes/frontend.js | 64 --- server/routes/index.js | 37 -- server/service/github-token.js | 34 -- server/service/github-userinfo.js | 58 --- server/service/index.js | 15 - server/service/model-update.js | 191 -------- server/service/netease-music.js | 100 ---- server/util/debug.js | 46 -- server/util/encrypt.js | 61 --- server/util/gravatar.js | 28 -- server/util/index.js | 91 ---- server/util/location.js | 23 - server/util/marked.js | 121 ----- server/util/proxy.js | 15 - server/util/sign-token.js | 15 - test/.gitkeep | 0 test/app/controller/home.test.js | 21 + 91 files changed, 235 insertions(+), 5155 deletions(-) delete mode 100644 . dockerignore create mode 100644 .autod.conf.js create mode 100644 .editorconfig create mode 100644 .eslintrc delete mode 100644 .eslintrc.js create mode 100644 .travis.yml delete mode 100644 Dockerfile delete mode 100644 LICENSE create mode 100644 README.zh-CN.md delete mode 100644 api.md create mode 100644 app/controller/home.js rename logs/.gitkeep => app/model/article.js (100%) create mode 100644 app/router.js create mode 100644 appveyor.yml delete mode 100755 bin/www create mode 100644 config/config.default.js create mode 100644 config/plugin.js delete mode 100644 docker-start.sh delete mode 100644 server/app.js delete mode 100644 server/config/development.js delete mode 100644 server/config/index.js delete mode 100644 server/config/production.js delete mode 100644 server/config/test.js delete mode 100644 server/controller/aliyun.js delete mode 100644 server/controller/article.js delete mode 100644 server/controller/auth.js delete mode 100644 server/controller/category.js delete mode 100644 server/controller/comment.js delete mode 100644 server/controller/index.js delete mode 100644 server/controller/moment.js delete mode 100644 server/controller/music.js delete mode 100644 server/controller/notification.js delete mode 100644 server/controller/option.js delete mode 100644 server/controller/statistics.js delete mode 100644 server/controller/tag.js delete mode 100644 server/controller/user.js delete mode 100644 server/middleware/authenticate.js delete mode 100644 server/middleware/error.js delete mode 100644 server/middleware/formidable.js delete mode 100644 server/middleware/header.js delete mode 100644 server/middleware/index.js delete mode 100644 server/middleware/response.js delete mode 100644 server/model/index.js delete mode 100644 server/model/schema/article.js delete mode 100644 server/model/schema/category.js delete mode 100644 server/model/schema/comment.js delete mode 100644 server/model/schema/index.js delete mode 100644 server/model/schema/moment.js delete mode 100644 server/model/schema/notification.js delete mode 100644 server/model/schema/option.js delete mode 100644 server/model/schema/tag.js delete mode 100644 server/model/schema/user.js delete mode 100644 server/plugins/akismet.js delete mode 100644 server/plugins/crontab.js delete mode 100644 server/plugins/index.js delete mode 100644 server/plugins/mailer.js delete mode 100644 server/plugins/mongo.js delete mode 100644 server/plugins/redis.js delete mode 100644 server/plugins/validation.js delete mode 100644 server/proxy/article.js delete mode 100644 server/proxy/base.js delete mode 100644 server/proxy/category.js delete mode 100644 server/proxy/comment.js delete mode 100644 server/proxy/index.js delete mode 100644 server/proxy/moment.js delete mode 100644 server/proxy/notification.js delete mode 100644 server/proxy/option.js delete mode 100644 server/proxy/tag.js delete mode 100644 server/proxy/user.js delete mode 100644 server/routes/backend.js delete mode 100644 server/routes/frontend.js delete mode 100644 server/routes/index.js delete mode 100644 server/service/github-token.js delete mode 100644 server/service/github-userinfo.js delete mode 100644 server/service/index.js delete mode 100644 server/service/model-update.js delete mode 100644 server/service/netease-music.js delete mode 100644 server/util/debug.js delete mode 100644 server/util/encrypt.js delete mode 100644 server/util/gravatar.js delete mode 100644 server/util/index.js delete mode 100644 server/util/location.js delete mode 100644 server/util/marked.js delete mode 100644 server/util/proxy.js delete mode 100644 server/util/sign-token.js delete mode 100644 test/.gitkeep create mode 100644 test/app/controller/home.test.js diff --git a/. dockerignore b/. dockerignore deleted file mode 100644 index 00cd03f..0000000 --- a/. dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -.DS_Store -npm-debug.log* -package-lock.json -.nuxt/ - -.vscode -.idea \ No newline at end of file diff --git a/.autod.conf.js b/.autod.conf.js new file mode 100644 index 0000000..7c5ea4e --- /dev/null +++ b/.autod.conf.js @@ -0,0 +1,30 @@ +'use strict'; + +module.exports = { + write: true, + prefix: '^', + plugin: 'autod-egg', + test: [ + 'test', + 'benchmark', + ], + dep: [ + 'egg', + 'egg-scripts', + ], + devdep: [ + 'egg-ci', + 'egg-bin', + 'egg-mock', + 'autod', + 'autod-egg', + 'eslint', + 'eslint-config-egg', + 'webstorm-disable-index', + ], + exclude: [ + './test/fixtures', + './dist', + ], +}; + diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e291365 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintignore b/.eslintignore index 926675b..4ebc8ae 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1 @@ -node_modules/* -test/* +coverage diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..60cc69f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "eslint-config-egg", + "rules": { + "indent": 4 + } +} \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 8193f62..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - root: true, - env: { - es6: true, - node: true - }, - extends: 'koa', - rules: { - "no-tabs": 0, - "indent": ["error", 'tab'], - 'arrow-parens': [2, 'as-needed'], - eqeqeq: 0, - 'no-return-assign': 0, // fails for arrow functions - 'no-var': 2, - semi: [0, 'always'], - 'space-before-function-paren': [2, 'always'], - yoda: 0, - 'arrow-spacing': 2, - 'dot-location': [2, 'property'], - 'prefer-arrow-callback': 2, - "prefer-promise-reject-errors": 0 - }, - globals: {} -} diff --git a/.gitignore b/.gitignore index a838a2b..14365a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,12 @@ -# Logs -logs -*.log - -# Runtime data -pids -*.pid -*.seed - -# Dependency directory -# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- -node_modules - -# Coverage directory used by tools like istanbul -coverage - -# Debug log from npm +logs/ npm-debug.log - -# IDEA -.vscode -.idea - -.DS_Store +yarn-error.log +node_modules/ package-lock.json - -# pm2 -ecosystem.config.js +yarn.lock +coverage/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b735efc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +sudo: false +language: node_js +node_js: + - '8' +install: + - npm i npminstall && npminstall +script: + - npm run ci +after_script: + - npminstall codecov && codecov diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a8f9f13..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM node:latest - -LABEL author="Jooger " - -RUN npm install pm2 -g - -ADD . /www/app/node-server/ - -WORKDIR /www/app/node-server - -RUN npm i - -ENV HOST 0.0.0.0 -ENV PORT 3001 -ENV NODE_ENV production - -RUN ["chmod", "+x", "/www/app/node-server/docker-start.sh"] -CMD /bin/bash /www/app/node-server/docker-start.sh $NODE_ENV - -EXPOSE 3001 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 453fb94..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Jooger - -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/README.md b/README.md index 6a13903..9e4064f 100644 --- a/README.md +++ b/README.md @@ -1,127 +1,33 @@ -[![GitHub forks](https://img.shields.io/github/forks/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/network) -[![GitHub stars](https://img.shields.io/github/stars/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/stargazers) -[![GitHub issues](https://img.shields.io/github/issues/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/issues) -[![GitHub last commit](https://img.shields.io/github/last-commit/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/commits/master) +# node-server -## node-server -⚡️ My blog's api server build with koa2 and mongoose,a RESTful application. -## Online site +## QuickStart -* jooger.me: [https://jooger.me](https://jooger.me) + -* node-server: [https://api.jooger.me](https://api.jooger.me) +see [egg docs][egg] for more detail. -* jooger.me-admin: [https://admin.jooger.me](https://admin.jooger.me) +### Development -## Build Setup - -``` bash -# install dependencies -$ npm install # Or yarn install - -# serve at localhost:3001 in development env +```bash +$ npm i $ npm run dev - -# serve at localhost:3001 in production env -$ npm run prod - -# serve with pm2 in development env -$ npm run pm2 - -# serve with pm2 in production env -$npm run pm2:prod - -# run pm2 deploy, need ecosystem.config.js at root path -$ npm run deploy - -# test code (TODO) -$ npm run test +$ open http://localhost:7001/ ``` -## Directory tree +### Deploy +```bash +$ npm start +$ npm stop ``` -node-server -|____api.md // api文档(待完善) -|____bin // 启动目录 -|____ecosystem.config.js // pm2启动文件,需要自己手动创建 -|____LICENSE // LICENSE(MIT) -|____logs // 日志目录,在ecosystem.config.js中配置 -|____server // 程序主目录 -| |____app.js // App程序入口 -| |____config // 配置文件目录 -| | |____development.js // 开发环境配置 -| | |____production.js // 生产环境配置 -| | |____test.js // 测试环境配置 -| |____controller // Controllers -| |____middleware // Koa中间件 -| |____model // 数据持久化模型 -| |____plugins // 插件目录 -| | |____akismet.js // 评论反垃圾 -| | |____mailer.js // 邮件客户端 -| | |____mongo.js // MongoDB驱动(mongoose) -| | |____redis.js // Redis -| | |____validation.js // 额外的校验规则 -| | |____gc.js // GC -| |____routes // 路由目录 -| | |____backend.js // 后台路由 -| | |____frontend.js // 前台路由 -| |____service // 服务目录 -| | |____crontab.js // 定时更新任务 -| | |____github-passport.js // Github验证 -| | |____github-userinfo.js // 获取Github用户信息 -| | |____github-token.js // 获取Github登录token -| | |____netease-music.js // 网易云音乐api -| |____proxy // model操作代理 -| |____util // 常用工具 -|____test // 测试目录 - -``` - -## TODOS - -* ~~音乐api~~ (2017.9.26) - -* ~~Github oauth 代理~~ (2017.9.28) - -* ~~文章分类api~~ (2017.10.26) - -* ~~Redis缓存部分数据~~ (2017.10.27 v1.1) - -* ~~评论api~~ (2017.10.28) - -* ~~评论定位 [geoip](https://github.com/bluesmoon/node-geoip)~~ (2017.10.29) - -* ~~垃圾评论过滤 [akismet](https://github.com/chrisfosterelli/akismet-api)~~ (2017.10.29) - -* ~~用户禁言~~ (2017.10.29) - -* ~~评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer)~~ (2017.10.29) - -* ~~GC优化~~ (2017.10.30,linux下需要预先安装g++, **已废弃**) - -* ~~个人动态api~~ (2017.10.30) - -* ~~文章归档api~~(2018.01.04) - -* ~~Model代理~~ (2018.01.28) - -* ~~ESlint~~ (2018.02.01) - -* ~~Docker支持~~ (2018.02.09) - -* ~~站内通知api~~ (2018.02.12) - -* 邮件模板 - -* TypeScript升级 -* 统计api +### npm scripts -* 完善API文档 +- Use `npm run lint` to check code style. +- Use `npm test` to run unit test. +- Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail. -* 测试case -* GraphQL +[egg]: https://eggjs.org \ No newline at end of file diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..dea4ee8 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,39 @@ +# node-server + + + +## 快速入门 + + + +如需进一步了解,参见 [egg 文档][egg]。 + +### 本地开发 + +```bash +$ npm i +$ npm run dev +$ open http://localhost:7001/ +``` + +### 部署 + +```bash +$ npm start +$ npm stop +``` + +### 单元测试 + +- [egg-bin] 内置了 [mocha], [thunk-mocha], [power-assert], [istanbul] 等框架,让你可以专注于写单元测试,无需理会配套工具。 +- 断言库非常推荐使用 [power-assert]。 +- 具体参见 [egg 文档 - 单元测试](https://eggjs.org/zh-cn/core/unittest)。 + +### 内置指令 + +- 使用 `npm run lint` 来做代码风格检查。 +- 使用 `npm test` 来执行单元测试。 +- 使用 `npm run autod` 来自动检测依赖更新,详细参见 [autod](https://www.npmjs.com/package/autod) 。 + + +[egg]: https://eggjs.org diff --git a/api.md b/api.md deleted file mode 100644 index a24c3a9..0000000 --- a/api.md +++ /dev/null @@ -1,74 +0,0 @@ -## Api - -Api文档 - -### 文章 - -> GET /articles 前台-文章列表 - -> GET /articles/:id 前台-文章详情 - -> GET /backend/articles 后台-文章列表 - -> GET /backend/articles/:id 后台-文章详情 - -> POST /backend/articles 后台-创建文章 - -> PATCH /backend/articles/:id 后台-修改文章 - -> DELETE /backend/articles/:id 后台-删除文章 - - -### 标签 - -> GET /tags 前台-标签列表 - -> GET /tags/:id 前台-标签详情 - -> GET /backend/tags 后台-标签列表 - -> GET /backend/tags/:id 后台-标签详情 - -> POST /backend/tags 后台-创建标签 - -> PATCH /backend/tags/:id 后台-修改标签 - -> DELETE /backend/tags/:id 后台-删除标签 - -### 评论 - -> GET /comments 前台-评论列表 - -> GET /comments/:id 前台-评论详情 - -> POST /comments 前台-发布评论 - -> GET /backend/comments 后台-评论列表 - -> GET /backend/comments/:id 后台-评论详情 - -> POST /backend/comments 后台-创建评论 - -> PATCH /backend/comments/:id 后台-修改评论 - -> DELETE /backend/comments/:id 后台-删除评论 - -### 全站配置 - -> GET /option 前台-全站配置 - -> GET /backend/option 后台-全站配置 - -> PATCH /backend/option 后台-修改全站配置 - -### 音乐 - -> GET /fronend/music/songs 前台-歌曲列表 - -> GET /fronend/music/songs/:id 前台-歌曲详情 - -> GET /fronend/music/songs/:id/url 前台-歌曲地址 - -> GET /fronend/music/songs/:id/lyric 前台-歌曲歌词 - -> GET /fronend/music/songs/:id/cover 前台-歌曲封面 diff --git a/app/controller/home.js b/app/controller/home.js new file mode 100644 index 0000000..f900fd4 --- /dev/null +++ b/app/controller/home.js @@ -0,0 +1,11 @@ +'use strict'; + +const Controller = require('egg').Controller; + +class HomeController extends Controller { + async index() { + this.ctx.body = 'hi, egg'; + } +} + +module.exports = HomeController; diff --git a/logs/.gitkeep b/app/model/article.js similarity index 100% rename from logs/.gitkeep rename to app/model/article.js diff --git a/app/router.js b/app/router.js new file mode 100644 index 0000000..7beabc2 --- /dev/null +++ b/app/router.js @@ -0,0 +1,9 @@ +'use strict'; + +/** + * @param {Egg.Application} app - egg application + */ +module.exports = app => { + const { router, controller } = app; + router.get('/', controller.home.index); +}; diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..c274b7d --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,14 @@ +environment: + matrix: + - nodejs_version: '8' + +install: + - ps: Install-Product node $env:nodejs_version + - npm i npminstall && node_modules\.bin\npminstall + +test_script: + - node --version + - npm --version + - npm run test + +build: off diff --git a/bin/www b/bin/www deleted file mode 100755 index fe53d79..0000000 --- a/bin/www +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -const http = require('http') -const app = require('../server/app') -const debug = require('../server/util').getDebug('App') -const config = require('../server/config') - -/** - * Get port from environment and store in Express. - */ - -const port = normalizePort(config.port) - -/** - * Create HTTP server. - */ - -const server = http.createServer(app.callback()) - -/** - * Listen on provided port, on all network interfaces. - */ - -server.listen(port) -server.on('error', onError) -server.on('listening', onListening) - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - const port = parseInt(val, 10) - - if (isNaN(port)) { - // named pipe - return val - } - - if (port >= 0) { - // port number - return port - } - - return false -} - -/** - * Event listener for HTTP server "error" event. - */ - -function onError(error) { - if (error.syscall !== 'listen') { - throw error - } - - const bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - debug(bind + ' requires elevated privileges') - process.exit(1) - break - case 'EADDRINUSE': - debug(bind + ' is already in use') - process.exit(1) - break - default: - throw error - } -} - -/** - * Event listener for HTTP server "listening" event. - */ - -function onListening() { - const addr = server.address() - const bind = typeof addr === 'string' - ? '管道:' + addr - : '端口:' + addr.port - debug.success('启动成功,', bind) -} diff --git a/config/config.default.js b/config/config.default.js new file mode 100644 index 0000000..1b17662 --- /dev/null +++ b/config/config.default.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = appInfo => { + const config = exports = {}; + + // use for cookie sign key, should change to your own and keep security + config.keys = appInfo.name + '_1534765762288_2697'; + + // add your config here + config.middleware = []; + + config.mongoose = {}; + + return config; +}; diff --git a/config/plugin.js b/config/plugin.js new file mode 100644 index 0000000..8da27c1 --- /dev/null +++ b/config/plugin.js @@ -0,0 +1,9 @@ +'use strict'; + +// had enabled by egg +// exports.static = true; + +exports.mongoose = { + enable: true, + package: 'egg-mongoose' +} diff --git a/docker-start.sh b/docker-start.sh deleted file mode 100644 index 2329c22..0000000 --- a/docker-start.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!bin/sh - -NODE_ENV=$1 - -if [ -z $NODE_ENV ] -then echo "请输入环境" -exit 1 -fi - -echo $NODE_ENV - -pm2 startOrReload ecosystem.config.js --env $NODE_ENV --no-daemon diff --git a/package.json b/package.json index fe7329c..b1b4cf9 100644 --- a/package.json +++ b/package.json @@ -1,96 +1,45 @@ { "name": "node-server", - "version": "1.5.0", + "version": "1.0.0", + "description": "", "private": true, - "description": "🔥 My blog's api server build by koa2 and mongoose", - "homepage": "https://github.com/jo0ger/node-server", - "author": { - "name": "jo0ger", - "email": "iamjooger@gmail.com", - "url": "https://jooger.me" + "dependencies": { + "egg": "^2.2.1", + "egg-mongoose": "^3.1.0", + "egg-scripts": "^2.5.0" }, - "repository": { - "type": "https", - "url": "https://github.com/jo0ger/node-server.git" + "devDependencies": { + "autod": "^3.0.1", + "autod-egg": "^1.0.0", + "egg-bin": "^4.3.5", + "egg-ci": "^1.8.0", + "egg-mock": "^3.14.0", + "eslint": "^4.11.0", + "eslint-config-egg": "^6.0.0", + "webstorm-disable-index": "^1.2.0" }, - "keywords": [ - "jooger.me", - "server", - "api", - "Nodejs", - "Koa2", - "MongoDB" - ], - "license": "MIT", - "bugs": { - "url": "https://github.com/jo0ger/node-server/issues" + "engines": { + "node": ">=8.9.0" }, - "bin": "./node_modules/.bin/", "scripts": { - "start": "cross-env NODE_ENV=development nodemon bin/www", - "debug": "cross-env NODE_ENV=development nodemon --inspect bin/www", - "prod": "cross-env NODE_ENV=production nodemon bin/www", - "pm2": "pm2 startOrReload ecosystem.config.js", - "prod:pm2": "pm2 startOrReload ecosystem.config.js --env production", - "deploy": "pm2 deploy ecosystem.config.js production", - "precommit": "npm run lint", - "lint": "eslint --ext .js,.ts --ignore-path .gitignore ." + "start": "egg-scripts start --daemon --title=egg-server-node-server", + "stop": "egg-scripts stop --title=egg-server-node-server", + "dev": "egg-bin dev", + "debug": "egg-bin debug", + "test": "npm run lint -- --fix && npm run test-local", + "test-local": "egg-bin test", + "cov": "egg-bin cov", + "lint": "eslint .", + "ci": "npm run lint && npm run cov", + "autod": "autod" }, - "dependencies": { - "akismet-api": "^3.0.0", - "axios": "^0.16.2", - "bcryptjs": "^2.4.3", - "big-integer": "^1.6.25", - "crypto": "^1.0.1", - "debug": "^2.6.9", - "formidable": "^1.1.1", - "geoip-lite": "^1.2.1", - "gravatar": "^1.6.0", - "highlight.js": "^9.12.0", - "jsonwebtoken": "^8.1.0", - "koa": "^2.4.1", - "koa-bodyparser": "^3.2.0", - "koa-bouncer": "^6.0.0", - "koa-bunyan-logger": "^2.0.0", - "koa-compose": "^4.0.0", - "koa-compress": "^2.0.0", - "koa-json": "^2.0.2", - "koa-logger": "^2.0.1", - "koa-onerror": "^1.2.1", - "koa-router": "^7.1.1", - "koa-session": "^5.5.0", - "lodash": "^4.17.4", - "marked": "^0.3.12", - "mongoose": "^4.13.9", - "mongoose-paginate": "^5.0.3", - "nodemailer": "^4.4.1", - "passport-github": "^1.1.0", - "redis": "^2.8.0", - "simple-netease-cloud-music": "^0.3.4", - "validator": "^9.2.0" + "ci": { + "version": "8" }, - "devDependencies": { - "cross-env": "^5.0.5", - "eslint": "^4.16.0", - "eslint-config-koa": "^2.0.2", - "eslint-config-standard": "^11.0.0-beta.0", - "eslint-plugin-import": "^2.8.0", - "eslint-plugin-node": "^5.2.1", - "eslint-plugin-promise": "^3.6.0", - "eslint-plugin-standard": "^3.0.1", - "nodemon": "^1.8.1", - "pre-git": "^3.17.0" - }, - "release": { - "analyzeCommits": "simple-commit-message" + "repository": { + "type": "git", + "url": "" }, - "config": { - "pre-git": { - "enabled": true, - "pre-commit": [ - "npm run precommit" - ], - "post-commit": "git status" - } - } + "author": "", + "license": "MIT" } diff --git a/server/app.js b/server/app.js deleted file mode 100644 index 8d8c4ff..0000000 --- a/server/app.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @desc Server entrance - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const Koa = require('koa') -const json = require('koa-json') -const logger = require('koa-logger') -const onerror = require('koa-onerror') -const bouncer = require('koa-bouncer') -const session = require('koa-session') -const compress = require('koa-compress') -const bodyparser = require('koa-bodyparser') -const koaBunyanLogger = require('koa-bunyan-logger') -const packageInfo = require('../package.json') -const middlewares = require('./middleware') -const routes = require('./routes') -const config = require('./config') -const { mongo, redis, akismet, validation, mailer, crontab } = require('./plugins') -const isProd = process.env.NODE_ENV === 'production' - -const app = new Koa() -app.keys = config.auth.secrets - -// load custom validations -bouncer.Validator = validation - -// error handler -onerror(app) - -// middlewares -app.use(bodyparser({ - enableTypes: ['json', 'form', 'text'] -})) -app.use(json()) -app.use(logger()) -app.use(koaBunyanLogger({ - name: packageInfo.name, - level: 'debug' -})) -app.use(koaBunyanLogger.requestIdContext({ - header: 'Request-Id' -})) -app.use(bouncer.middleware()) -app.use(middlewares.response) -app.use(middlewares.error) -// form parse -// app.use(middlewares.formidable()) -app.use(session(config.auth.session, app)) -app.use(compress()) - -// routes -routes(app) - -// connect mongodb -mongo.connect() - -// connect redis -redis.connect() - -if (isProd) { - // akismet - akismet.start() - - // mailer - mailer.start() -} - -// crontab -crontab.start() - -module.exports = app diff --git a/server/config/development.js b/server/config/development.js deleted file mode 100644 index f586da8..0000000 --- a/server/config/development.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @desc 开发环境配置 - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -module.exports = { - mongo: { - uri: 'mongodb://127.0.0.1/jooger-me-dev' - }, - sns: { - github: { - // 测试用的ID和Secret - clientID: '5b4d4a7945347d0fd2e2', - clientSecret: '8771bd9ae52749cc15b0c9e2c6cb4ecd7f39d9da', - callbackURL: 'http://127.0.0.1:3001/auth/github/login/callback' - } - } -} diff --git a/server/config/index.js b/server/config/index.js deleted file mode 100644 index 643027f..0000000 --- a/server/config/index.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * @desc Config entry - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const path = require('path') -const _ = require('lodash') -const packageInfo = require('../../package.json') - -const baseConfig = { - name: packageInfo.name, - version: packageInfo.version, - author: packageInfo.author.name, - site: packageInfo.author.url, - email: packageInfo.author.email, - email_163: 'zzy1198258955@163.com', - env: process.env.NODE_ENV, - root: path.resolve(__dirname, '../../'), - port: process.env.PORT || 3001, - // 限制参数 - limit: { - articleLimit: 15, - // 相关文章限制个数 - relatedArticleLimit: 10, - hotLimit: 7, - commentLimit: 20, - momentLimit: 10, - // 垃圾评论允许的最大发布次数 - commentSpamLimit: 3 - }, - mongo: { - option: { - useMongoClient: true, - poolSize: 20, - keepAlive: true, - autoReconnect: true, - reconnectInterval: 1000, - reconnectTries: Number.MAX_VALUE - } - }, - redis: { - host: '127.0.0.1', - port: 6379 - }, - auth: { - session: { - key: 'jooger.me.token', - maxAge: 60000 * 60 * 24 * 7, - signed: false - }, - userCookieKey: 'jooger.me.userid', - secrets: `${packageInfo.name}-secrets`, - defaultAvatar: 'http://static.jooger.me/img/common/default-avatar.png', - // 初始化管理员,默认github账户名 - defaultName: packageInfo.author.name, - defaultPassword: 'admin_jooger' - }, - sns: { - github: { - // 登陆后的token的cookie名,每个第三方登录方式必备项 - key: 'jooger.me.github.token', - clientID: process.env.githubClientID || 'github client id', - clientSecret: process.env.githubClientSecret || 'github client secret', - callbackURL: 'github oauth callback url' - } - }, - akismet: { - apiKey: process.env.akismetApikey || 'akismet api key' - }, - aliyun: { - oss: { - accessKeyId: process.env.aliyunOssAccessKeyId || 'alayu accesskey Id', - accessKeySecret: process.env.aliyunOssAccessKeySecret || 'aliyun oss accesskey secret', - bucket: 'jooger-static', - region: 'oss-cn-beijing' - } - }, - constant: { - // 允许请求的域名 - allowedOrigins: [ - 'jooger.me', - 'www.jooger.me', - 'admin.jooger.me' - ], - codeMap: { - '-1': '请求失败', - '200': '请求成功', - '401': '权限校验失败', - '403': 'Forbidden', - '500': '服务器错误', - '10001': '参数错误' - }, - // 角色 - roleMap: { - ADMIN: 0, - USER: 1, - GITHUB_USER: 2 - }, - monthMap: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], - // 站内通知 - notification: { - typeMap: { - GENERAL: 0, - COMMENT: 1, - LIKE: 2, - USER: 3 - }, - categoryMap: { - // type === 0,系统通知 - MUTE_USER: 'mute-user', // 用户禁言 - // type === 1,评论通知 - COMMENT_COMMENT: 'comment-comment', // 评论(非回复) - COMMENT_REPLY: 'comment-reply', // 评论回复 - COMMENT_UPDATE: 'comment-update', // 评论更新 - // type === 2,点赞通知 - LIKE_ARTICLE: 'like-article', // 文章点赞 - UNLIKE_ARTICLE: 'unlike-article', // 文章取消点赞 - LIKE_COMMENT: 'like-comment', // 评论点赞 - UNLIKE_COMMENT: 'unlike-comment', // 评论取消点赞 - // type === 3, 用户操作通知 - USER_CREATE: 'user-create', // 用户创建 - USER_UPDATE: 'user-update' // 用户更新 - } - }, - redisCacheKey: { - music: 'music-data' - } - } -} - -module.exports = _.merge(baseConfig, require(`./${process.env.NODE_ENV}`)) diff --git a/server/config/production.js b/server/config/production.js deleted file mode 100644 index df8eab4..0000000 --- a/server/config/production.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @desc 开发环境配置 - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const isDocker = process.env.RUN_ENV === 'docker' - -module.exports = { - mongo: { - uri: `mongodb://${isDocker ? 'mongo' : '127.0.0.1'}/jooger-me` - }, - redis: { - host: isDocker ? 'redis' : '127.0.0.1' - }, - auth: { - session: { - domain: '.jooger.me' - } - }, - sns: { - github: { - callbackURL: 'https://api.jooger.me/auth/github/login/callback' - } - } -} diff --git a/server/config/test.js b/server/config/test.js deleted file mode 100644 index 5e15e20..0000000 --- a/server/config/test.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @desc 测试环境配置 - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -module.exports = {} diff --git a/server/controller/aliyun.js b/server/controller/aliyun.js deleted file mode 100644 index 0de64b9..0000000 --- a/server/controller/aliyun.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @desc Aliyun controller - * @author Jooger - * @date 26 Sep 2017 - */ - -'use strict' - -const config = require('../config') -const oss = config.aliyun.oss - -exports.oss = async (ctx, next) => { - oss - ? ctx.success({ - accessKeyId: 'LTAIMh28MLnWG7MA', - accessKeySecret: 'B0v7JCx65VmtNws22BzFnTQcX2kzm9', - bucket: oss.bucket, - region: oss.region - }, '阿里云OSS参数获取成功') - : ctx.fail('阿里云OSS参数获取失败') -} diff --git a/server/controller/article.js b/server/controller/article.js deleted file mode 100644 index e04d0d5..0000000 --- a/server/controller/article.js +++ /dev/null @@ -1,451 +0,0 @@ -/** - * @desc Article controller - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const config = require('../config') -const { articleProxy, categoryProxy, tagProxy, userProxy } = require('../proxy') -const { marked, isObjectId, createObjectId, getMonthFromNum, getDocsPaginationData } = require('../util') - -// 文章列表 -exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.articleLimit).toInt().gt(0, 'per_page参数必须大于0').val() - const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'page参数必须大于0').val() - const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'state参数错误').val() - const category = ctx.validateQuery('category').optional().toString().val() - const tag = ctx.validateQuery('tag').optional().toString().val() - const keyword = ctx.validateQuery('keyword').optional().toString().val() - // 时间区间查询仅后台可用,且依赖于createdAt - const startDate = ctx.validateQuery('start_date').optional().toString().val() - const endDate = ctx.validateQuery('end_date').optional().toString().val() - // 排序仅后台能用,且order和sortBy需同时传入才起作用 - // -1 desc | 1 asc - const order = ctx.validateQuery('order').optional().toInt().isIn( - [-1, 1], - 'order参数错误' - ).val() - // createdAt | updatedAt | publishedAt | meta.ups | meta.pvs | meta.comments - const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn( - ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], - 'sort_by参数错误' - ).val() - - // 过滤条件 - const options = { - sort: { - updatedAt: -1, - createdAt: -1 - }, - page, - limit: pageSize, - select: '-content -renderedContent', - populate: [ - { - path: 'category', - select: 'name description extends' - }, { - path: 'tag', - select: 'name description' - } - ] - } - - // 查询条件 - const query = {} - - if (state !== undefined) { - query.state = state - } - - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { title: keywordReg } - ] - } - - // 分类 - if (category) { - // 如果是id - if (isObjectId(category)) { - query.category = category - } else { - // 普通字符串,需要先查到id - const c = await categoryProxy.findOne({ name: category }).exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - query.category = c ? c._id : createObjectId() - } - } - - // 标签 - if (tag) { - // 如果是id - if (isObjectId(tag)) { - query.tag = tag - } else { - // 普通字符串,需要先查到id - const t = await tagProxy.findOne({ name: tag }).exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - query.tag = t ? t._id : createObjectId() - } - } - - // 未通过权限校验(前台获取文章列表) - if (!ctx._isAuthenticated) { - // 将文章状态重置为1 - query.state = 1 - // 文章列表不需要content和state - options.select = '-content -renderedContent -state' - } else { - // 排序 - if (sortBy && order) { - options.sort = {} - options.sort[sortBy] = order - } - - // 起始日期 - if (startDate) { - const $gte = new Date(startDate) - if ($gte.toString() !== 'Invalid Date') { - query.createdAt = { $gte } - } - } - - // 结束日期 - if (endDate) { - const $lte = new Date(endDate) - if ($lte.toString() !== 'Invalid Date') { - query.createdAt = Object.assign({}, query.createdAt, { $lte }) - } - } - } - - const articles = await articleProxy.paginate(query, options) - - articles - ? ctx.success(getDocsPaginationData(articles), '文章列表获取成功') - : ctx.fail('文章列表获取失败') -} - -// 热门文章 -exports.hot = async (ctx, next) => { - const limit = ctx.validateQuery('limit').defaultTo(config.limit.hotLimit).toInt().gt(0, 'limit参数必须大于0').val() - const data = await articleProxy.find() - .sort('-meta.comments -meta.ups -meta.pvs') - .select('-content -renderedContent -state') - .populate([ - { - path: 'category', - select: 'name' - }, { - path: 'tag', - select: 'name' - } - ]) - .limit(limit) - data - ? ctx.success({ list: data }, '热门文章获取成功') - : ctx.fail('热门文章获取失败') -} - -// 文章详情 -exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() - - let data = null - let query = null - // 只有前台博客访问文章的时候pv才+1 - if (!ctx._isAuthenticated) { - query = articleProxy.updateOne({ _id: id, state: 1 }, { $inc: { 'meta.pvs': 1 } }).select('-content') - } else { - query = articleProxy.getById(id) - } - - data = await query.populate([ - { - path: 'category', - select: 'name description extends' - }, { - path: 'tag', - select: 'name description extends' - } - ]).exec() - - if (data) { - data = data.toObject() - await Promise.all([ - getRelatedArticles(ctx, data), - getSiblingArticles(ctx, data) - ]) - ctx.success(data, '文章详情获取成功') - } else { - ctx.fail('文章详情获取失败') - } -} - -// 文章创建 -exports.create = async (ctx, next) => { - const title = ctx.validateBody('title').required('缺少文章标题').notEmpty().val() - const content = ctx.validateBody('content').required('缺少文章内容').notEmpty().val() - const keywords = ctx.validateBody('keywords').optional().toArray().val() - const category = ctx.validateBody('category').optional().isObjectId().val() - const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() - const description = ctx.validateBody('description').optional().val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数错误').val() - const thumb = ctx.validateBody('thumb').optional().val() - const createdAt = ctx.validateBody('createdAt').optional().toString().val() - const permalink = ctx.validateBody('permalink').optional().val() - const article = {} - - title && (article.title = title) - keywords && (article.keywords = keywords) - description && (article.description = description) - category && (article.category = category) - tag && (article.tag = tag) - thumb && (article.thumb = thumb) - createdAt && (article.createdAt = new Date(createdAt)) - permalink && (article.permalink = permalink) - - if (state !== undefined) { - article.state = state - } - article.content = content - article.renderedContent = marked(content) - - let data = await articleProxy.newAndSave(article) - - if (data && data.length) { - data = data[0] - if (!data.permalink) { - // 更新永久链接 - data = await articleProxy.updateById(data._id, { - permalink: `${config.site}/article/${data._id}` - }).exec().catch(err => { - ctx.log.error('文章永久链接更新失败', err.message) - return data - }) - } - ctx.success(data, '文章创建成功') - } else { - ctx.fail('文章创建失败') - } -} - -// 文章更新 -exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() - const title = ctx.validateBody('title').optional().val() - const content = ctx.validateBody('content').optional().val() - const keywords = ctx.validateBody('keywords').optional().toArray().val() - const description = ctx.validateBody('description').optional().val() - const category = ctx.validateBody('category').optional().isObjectId().val() - const tag = ctx.validateBody('tag').optional().isObjectIdArray().val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数错误').val() - const thumb = ctx.validateBody('thumb').optional().val() - const createdAt = ctx.validateBody('createdAt').optional().toString().val() - const article = {} - - title && (article.title = title) - keywords && (article.keywords = keywords) - description && (article.description = description) - category && (article.category = category) - tag && (article.tag = tag) - thumb && (article.thumb = thumb) - createdAt && (article.createdAt = new Date(createdAt)) - - if (state !== undefined) { - article.state = state - } - - if (content !== undefined) { - article.content = content - article.renderedContent = marked(content) - } - - const data = await articleProxy.updateById(id, article).populate('category tag').exec() - - data - ? ctx.success(data, '文章更新成功') - : ctx.fail('文章更新失败') -} - -// 删除文章 -exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() - const data = await articleProxy.delById(id).exec() - - data && data.result && data.result.ok - ? ctx.success(null, '文章删除成功') - : ctx.fail('文章删除失败') -} - -// 文章点赞 -exports.like = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少文章ID').toString().isObjectId().val() - const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() - const user = ctx.validateBody('user').optional().isObjectId().val() - let userCache = null - if (user) { - userCache = await userProxy.getById(user).exec().catch(err => { - ctx.log.error(err.message) - return null - }) - } - - let data = null - if (!userCache || !!userCache.role) { - data = await articleProxy.likeAndNotify(id, like, userCache) - } else { - data = await articleProxy.updateById(id, { - $inc: { - 'meta.ups': like ? 1 : -1 - } - }).exec() - } - - data - ? ctx.success(null, '文章点赞成功') - : ctx.fail('文章点赞失败') -} - -// 文章归档 -exports.archives = async (ctx, next) => { - let data = await articleProxy.aggregate([ - { $match: { state: 1 } }, - { $sort: { createdAt: 1 } }, - { - $project: { - year: { $year: '$createdAt' }, - month: { $month: '$createdAt' }, - title: 1, - createdAt: 1 - } - }, - { - $group: { - _id: { - year: '$year', - month: '$month' - }, - articles: { - $push: { - title: '$title', - _id: '$_id', - createdAt: '$createdAt' - } - } - } - } - ]) - - let count = 0 - if (data && data.length) { - data = [...new Set(data.map(item => item._id.year))].map(year => { - const months = [] - data.forEach(item => { - const { _id, articles } = item - if (year === _id.year) { - count += articles.length - months.push({ - month: _id.month, - monthStr: getMonthFromNum(_id.month), - articles - }) - } - }) - return { - year, - months - } - }) - } - ctx.success({ - count, - list: data || [] - }, '获取文章归档成功') -} - -/** - * 根据标签获取相关文章 - * @param {} ctx koa ctx - * @param {} data 文章数据 - */ -async function getRelatedArticles (ctx, data) { - data.related = [] - let { _id, tag = [] } = data - const articles = await articleProxy.find({ - _id: { $nin: [ _id ] }, - state: 1, - tag: { $in: tag.map(t => t._id) } - }) - .select('title thumb createdAt publishedAt meta category') - .populate({ - path: 'category', - select: 'name description' - }) - .exec() - .catch(err => { - ctx.log.error('关联文章查询失败,err:', err.message) - return null - }) - - if (articles) { - // 最多取前10篇 - data.related = articles.slice(0, config.limit.relatedArticleLimit) - } -} - -/** - * 获取相邻的文章 - * @param {} ctx koa ctx - * @param {} data 文章数据 - */ -async function getSiblingArticles (ctx, data) { - if (data && data._id) { - const query = {} - // 如果未通过权限校验,将文章状态重置为1 - if (!ctx._isAuthenticated) { - query.state = 1 - } - const prev = await articleProxy.findOne(query) - .select('title createdAt publishedAt thumb category') - .populate({ - path: 'category', - select: 'name description' - }) - .sort('-createdAt') - .lt('createdAt', data.createdAt) - .exec() - .catch(err => { - ctx.log.error('前一篇文章获取失败,err:', err.message) - return null - }) - const next = await articleProxy.findOne(query) - .select('title createdAt publishedAt thumb category') - .populate({ - path: 'category', - select: 'name description' - }) - .sort('createdAt') - .gt('createdAt', data.createdAt) - .exec() - .catch(err => { - ctx.log.error('后一篇文章获取失败,err:', err.message) - return null - }) - data.adjacent = { - prev: prev ? prev.toObject() : null, - next: next ? next.toObject() : null - } - } -} diff --git a/server/controller/auth.js b/server/controller/auth.js deleted file mode 100644 index 25483b5..0000000 --- a/server/controller/auth.js +++ /dev/null @@ -1,155 +0,0 @@ -/** - * @desc Auth controller - * @author Jooger - * @date 27 Sep 2017 - */ - -'use strict' - -const config = require('../config') -const { userProxy } = require('../proxy') -const { bcompare, getDebug, signToken, proxy, randomString } = require('../util') -const { getGithubToken, getGithubAuthUserInfo } = require('../service') -const debug = getDebug('Auth') - -exports.localLogin = async (ctx, next) => { - const name = ctx.validateBody('name').required('缺少登录名').notEmpty().val() - const password = ctx.validateBody('password').required('缺少密码').notEmpty().val() - - const user = await userProxy.findOne({ name }).exec() - - if (user) { - const vertifyPassword = bcompare(password, user.password) - if (vertifyPassword) { - const { session } = config.auth - const token = signToken({ - id: user._id, - name: user.name - }) - ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - ctx.cookies.set(config.auth.userCookieKey, user._id, { signed: false, domain: session.domain, maxAge: session.maxAge, httpOnly: false }) - debug.success('登录成功, 用户ID:%s,用户名:%s', user._id, user.name) - ctx.success({ - id: user._id, - token - }, '登录成功') - } else { - ctx.fail('密码错误') - } - } else { - ctx.fail('用户不存在') - } -} - -exports.logout = async (ctx, next) => { - const { session } = config.auth - const token = signToken({ - id: ctx._user._id, - name: ctx._user.name - }, false) - ctx.cookies.set(session.key, token, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) - ctx.cookies.set(config.auth.userCookieKey, ctx._user._id, { signed: false, domain: session.domain, maxAge: 0, httpOnly: false }) - debug.success('登出成功, 用户ID:%s,用户名:%s', ctx.user._id, ctx.user.name) - ctx.success(null, '登出成功') -} - -exports.info = async (ctx, next) => { - const adminId = ctx._user._id - if (!adminId && !ctx._isSnsAuthenticated && !ctx._isAuthenticated) { - return ctx.fail(401) - } - let data = null - if (ctx._isSnsAuthenticated) { - // TODO: 第三方信息获取 - } else if (ctx._isAuthenticated) { - data = await userProxy.getById(adminId) - .select('-password') - .exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - } - - if (data) { - ctx.success({ - info: data, - token: ctx.session._token - }) - } else { - ctx.fail(401) - } -} - -exports.fetchGithubToken = async (ctx, next) => { - const code = ctx.validateQuery('code').required('缺少code参数').toString().val() - const token = await getGithubToken(code) - if (token) { - ctx.success(token) - } else { - ctx.fail('Token获取失败') - } -} - -exports.fetchGithubUser = async (ctx, next) => { - const accessToken = ctx.validateQuery('access_token').required('缺少access_token参数').toString().val() - const data = await getGithubAuthUserInfo(accessToken) - if (!data) { - return ctx.fail('用户信息获取失败') - } - const user = await createLocalUserFromGithub(data) - if (user) { - ctx.success(user) - } else { - ctx.fail('用户信息获取失败') - } -} - -async function createLocalUserFromGithub (githubUser) { - const user = await userProxy.findOne({ - 'github.id': githubUser.id - }).catch(err => { - debug.error('本地用户查找失败, 错误:', err.message) - return null - }) - if (user) { - const userData = { - name: githubUser.name || githubUser.login, - avatar: proxy(githubUser.avatar_url), - slogan: githubUser.bio, - github: githubUser, - role: user.role - } - const updatedUser = await userProxy.updateById(user._id, userData) - .select('-password -role -createdAt -updatedAt') - .exec().catch(err => { - debug.error('本地用户更新失败, 错误:', err.message) - }) || user - - return updatedUser.toObject() - } else { - const newUser = { - name: githubUser.name || githubUser.login, - avatar: proxy(githubUser.avatar_url), - slogan: githubUser.bio, - github: githubUser, - role: 1 - } - - const checkUser = await userProxy.findOne({ name: newUser.name }).exec().catch(err => { - debug.error('本地用户查找失败, 错误:', err.message) - return true - }) - - if (checkUser) { - newUser.name += '-' + randomString() - } - - const data = await userProxy.createAndNotify(newUser).catch(err => { - debug.error('本地用户创建失败, 错误:', err.message) - return null - }) - - return data && data.length ? data[0].toObject() : null - } -} diff --git a/server/controller/category.js b/server/controller/category.js deleted file mode 100644 index fdab5e7..0000000 --- a/server/controller/category.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * @desc Category controll - * @author Jooger - * @date 26 Oct 2017 - */ - -'use strict' - -const { articleProxy, categoryProxy } = require('../proxy') - -// 分类列表 -exports.list = async (ctx, next) => { - const keyword = ctx.validateQuery('keyword').optional().toString().val() - // 是否按照list属性排序 - const rank = ctx.validateQuery('rank').defaultTo(1).toInt().isIn([0, 1], 'rank参数错误').val() - - const query = {} - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { name: keywordReg } - ] - } - - let sort = '-createdAt' - if (rank) { - sort = 'list ' + sort - } - - const data = await categoryProxy.find(query).sort(sort) - - if (data) { - for (let i = 0; i < data.length; i++) { - if (typeof data[i].toObject === 'function') { - data[i] = data[i].toObject() - } - const articles = await articleProxy.find({ category: data[i]._id }).exec().catch(err => { - ctx.log.error(err.message) - return [] - }) - data[i].count = articles.length - } - ctx.success(data, '分类列表获取成功') - } else { - ctx.fail('分类列表获取失败') - } -} - -// 分类详情 -exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() - - let data = await categoryProxy.getById(id).exec() - - if (data) { - data = data.toObject() - const articles = await articleProxy.find({ category: id }) - .select('-category') - .exec() - .catch(err => { - ctx.log.error(err.message) - return [] - }) - data.articles = articles - data.articles_count = articles.length - ctx.success(data, '分类详情获取成功') - } else { - ctx.fail('分类详情获取失败') - } -} - -// 分类创建 -exports.create = async (ctx, next) => { - const name = ctx.validateBody('name').required('缺少分类名称').notEmpty().val() - const description = ctx.validateBody('description').optional().val() - const list = ctx.validateBody('list').defaultTo(1).toInt().val() - const ext = ctx.validateBody('extends').optional().toArray().val() - - const { length } = await categoryProxy.find({ name }).exec().catch(err => { - ctx.log.error(err.message) - return [] - }) - - if (!length) { - const data = await categoryProxy.newAndSave({ - name, - description, - extends: ext, - list - }) - - data && data.length - ? ctx.success(data[0], '分类创建成功') - : ctx.fail('分类创建失败') - } else { - ctx.fail(`【${name}】分类已经存在`) - } -} - -// 分类更新 -exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() - const name = ctx.validateBody('name').optional().val() - const description = ctx.validateBody('description').optional().val() - const list = ctx.validateBody('list').optional().toInt().val() - const ext = ctx.validateBody('extends').optional().toArray().val() - const category = {} - - name && (category.name = name) - description && (category.description = description) - list && (category.list = list) - ext && (category.extends = ext) - - const data = await categoryProxy.updateById(id, category).exec() - - data - ? ctx.success(data, '分类更新成功') - : ctx.fail('分类更新失败') -} - -// 删除分类 -exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少分类ID').toString().isObjectId().val() - const articles = await articleProxy.find({ category: id }).exec() - - if (articles && articles.length) { - // 分类下面有文章,不能删除 - ctx.fail('该分类下有文章,不能删除') - } else { - const data = await categoryProxy.delById(id).exec() - data && data.result && data.result.ok - ? ctx.success(null, '分类删除成功') - : ctx.fail('分类删除失败') - } -} diff --git a/server/controller/comment.js b/server/controller/comment.js deleted file mode 100644 index 6124b9d..0000000 --- a/server/controller/comment.js +++ /dev/null @@ -1,638 +0,0 @@ -/** - * @desc Comment controller - * @author Jooger - * @date 28 Oct 2017 - */ - -'use strict' - -const config = require('../config') -const { akismet, mailer } = require('../plugins') -const { commentProxy, userProxy, articleProxy } = require('../proxy') -const { isType, marked, isObjectId, createObjectId, getDebug, getLocation, gravatar, getDocsPaginationData } = require('../util') -const debug = getDebug('Comment') -const isProd = process.env.NODE_ENV === 'development' - -// 评论列表 -exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.commentLimit).toInt().gt(0, 'per_page参数必须大于0').val() - const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'page参数必须大于0').val() - const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'state参数错误').val() - const type = ctx.validateQuery('type').optional().toInt().isIn([0, 1], 'type参数错误').val() - const author = ctx.validateQuery('author').optional().toString().isObjectId().val() - const article = ctx.validateQuery('article').optional().toString().isObjectId().val() - const keyword = ctx.validateQuery('keyword').optional().toString().val() - const parent = ctx.validateQuery('parent').optional().toString().isObjectId().val() - // 时间区间查询仅后台可用,且依赖于createdAt - const startDate = ctx.validateQuery('start_date').optional().toString().val() - const endDate = ctx.validateQuery('end_date').optional().toString().val() - // 排序仅后台能用,且order和sortBy需同时传入才起作用 - // -1 desc | 1 asc - const order = ctx.validateQuery('order').optional().toInt().isIn([-1, 1], 'order参数错误').val() - // createdAt | updatedAt | ups - const sortBy = ctx.validateQuery('sort_by').optional().toString().isIn(['createdAt', 'updatedAt', 'ups'], 'sort_by参数错误').val() - - // 过滤条件 - const options = { - sort: { createdAt: 1 }, - page, - limit: pageSize, - select: '', - populate: [ - { - path: 'author', - select: !ctx._isAuthenticated ? 'github avatar name site' : '' - }, - { - path: 'parent', - select: 'author meta sticky ups', - match: { - state: 1 - } - }, - { - path: 'forward', - select: 'author meta sticky ups', - match: { - state: 1 - }, - populate: { - path: 'author', - select: 'avatar github name' - } - } - ] - } - - // 查询条件 - const query = {} - - if (type !== undefined) { - query.type = type - } - - if (state !== undefined) { - query.state = state - } - - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { content: keywordReg } - ] - } - - // 用户 - if (author) { - // 如果是id - if (isObjectId(author)) { - query.author = author - } else { - // 普通字符串,需要先查到id - const u = await userProxy.findOne({ name: author }).exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - query.author = u ? u._id : createObjectId() - } - } - - // 文章 - if (article) { - // 如果是id - if (isObjectId(article)) { - query.article = article - } else { - // 普通字符串,需要先查到id - const a = await articleProxy.findOne({ name: article }).exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - query.article = a ? a._id : createObjectId() - } - } - - // 排序 - if (sortBy && order) { - options.sort = {} - options.sort[sortBy] = order - } - - if (parent) { - // 获取子评论 - query.parent = parent - } else { - // 获取父评论 - query.parent = { $exists: false } - } - - // 未通过权限校验(前台获取评论列表) - if (!ctx._isAuthenticated) { - // 将评论状态重置为1 - query.state = 1 - query.spam = false - // 评论列表不需要content和state - options.select = '-content -state -updatedAt -spam -type' - } else { - // 起始日期 - if (startDate) { - const $gte = new Date(startDate) - if ($gte.toString() !== 'Invalid Date') { - query.createdAt = { $gte } - } - } - - // 结束日期 - if (endDate) { - const $lte = new Date(endDate) - if ($lte.toString() !== 'Invalid Date') { - query.createdAt = Object.assign({}, query.createdAt, { $lte }) - } - } - } - - const comments = await commentProxy.paginate(query, options) - - if (comments) { - const data = [] - // 查询子评论数量 - await Promise.all(comments.docs.map(doc => { - doc = doc.toObject() - doc.subCount = 0 - data.push(doc) - return commentProxy.count({ parent: doc._id }).exec() - .then(count => { - doc.subCount = count - }) - .catch(err => { - ctx.log.error(err) - doc.subCount = 0 - }) - })) - comments.docs = data - ctx.success(getDocsPaginationData(comments), '评论列表获取成功') - } else { - ctx.fail('评论列表获取失败') - } -} - -// 评论详情 -exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少评论ID').toString().isObjectId().val() - - let data = null - let queryPs = null - if (!ctx._isAuthenticated) { - queryPs = commentProxy.getById(id, { state: 1, spam: false }) - .select('-content -state -updatedAt -type -spam') - .populate({ - path: 'author', - select: 'github' - }) - .populate({ - path: 'parent', - select: 'author meta sticky ups' - }) - .populate({ - path: 'forward', - select: 'author meta sticky ups' - }) - } else { - queryPs = commentProxy.getById(id) - } - - data = await queryPs.populate([ - { - path: 'author', - select: 'github' - }, { - path: 'parent', - select: 'author meta sticky ups' - }, { - path: 'forward', - select: 'author meta sticky ups' - } - ]).exec() - - data - ? ctx.success(data.toObject(), '评论详情获取成功') - : ctx.fail('评论详情获取失败') -} - -// 发表评论 -exports.create = async (ctx, next) => { - const content = ctx.validateBody('content').required('缺少评论内容').notEmpty().val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数错误').val() - const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], 'sticky参数错误').val() - const type = ctx.validateBody('type').defaultTo(0).toInt().isIn([0, 1], 'type参数错误').val() - const article = ctx.validateBody('article').optional().toString().isObjectId('article参数错误').val() - const parent = ctx.validateBody('parent').optional().toString().isObjectId('parent参数错误').val() - const forward = ctx.validateBody('forward').optional().toString().isObjectId('forward参数错误').val() - // ObjectId | { id, name, email, site } - const author = ctx.validateBody('author').required('author参数错误').val() - const req = ctx.req - const comment = { content } - - if (type === undefined || type === 0) { - if (!article) { - return ctx.fail('缺少article参数') - } - comment.article = article - } - - if ((parent && !forward) || (!parent && forward)) { - return ctx.fail('缺少parent和forward参数') - } - - const user = await checkAuthor.call(ctx, author) - if (!user) { - return ctx.fail('作者不存在') - } else if (user.mute) { - // 如果被禁言 - return ctx.fail('你已经被禁言') - } - comment.author = user._id - - if (!checkUserSpam(user)) { - return ctx.fail('你的垃圾评论数量已达到最大限制,已被禁言') - } - const isAdmin = !user.role - - if (state !== undefined) { - comment.state = state - } - - if (type !== undefined) { - comment.type = type - } - - if (sticky !== undefined) { - comment.sticky = sticky - } - - const { ip, location } = getLocation(req) - comment.meta = {} - comment.meta.location = location || null - comment.meta.ip = ip - comment.meta.ua = req.headers['user-agent'] || '' - comment.meta.referer = req.headers.referer || '' - - // 先判断是不是垃圾邮件 - const akismetClient = akismet.getAkismetClient() - let isSpam = false - // 永链 - const permalink = getPermalink(comment) - if (akismetClient) { - isSpam = await akismetClient.checkSpam({ - user_ip: ip, - user_agent: comment.meta.ua, - referrer: comment.meta.referer, - permalink, - comment_type: getCommentType(type), - comment_author: user.name, - comment_author_email: user.email, - comment_author_url: user.site, - comment_content: content, - is_test: isProd - }) - } - - // 如果是Spam评论 - if (isSpam) { - return ctx.fail('检测为垃圾评论,该评论将不会显示') - } - - parent && (comment.parent = parent) - forward && (comment.forward = forward) - comment.renderedContent = marked(content) - - let data = await commentProxy[isAdmin ? 'newAndSave' : 'createAndNotify'](comment) - - if (data) { - let p = commentProxy.getById(data._id) - if (!ctx._isAuthenticated) { - p = p.select('-content -state -updatedAt') - .populate({ - path: 'author', - select: 'name site avatar role mute email' - }) - .populate({ - path: 'parent', - select: 'author meta sticky ups' - }) - .populate({ - path: 'forward', - select: 'author meta sticky ups' - }) - } - data = await p - .exec() - .catch(err => { - ctx.log.error(err.message) - return null - }) - ctx.success(data, '评论创建成功') - // 如果是文章评论,则更新文章评论数量 - if (type === 0) { - updateArticleCommentCount([comment.article]) - } - // 发送邮件通知站主和被评论者 - sendEmailToAdminAndUser(data, permalink) - } else { - ctx.fail('评论创建失败') - } -} - -// 评论更新 -exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('评论ID参数错误').toString().isObjectId().val() - const content = ctx.validateBody('content').optional().val() - const state = ctx.validateBody('state').optional().toInt().isIn([-2, 0, 1, 2], 'state参数错误').val() - const sticky = ctx.validateBody('sticky').optional().toInt().isIn([0, 1], 'sticky参数错误').val() - const comment = {} - - let cache = await commentProxy.getById(id) - .populate('author') - .exec() - if (!cache) { - return ctx.fail('评论不存在') - } - cache = cache.toObject() - if (ctx._isAuthenticated && ctx._user._id.toString() !== cache.author._id.toString()) { - return ctx.fail('其他人的评论内容不能修改') - } - - if (content !== undefined) { - comment.content = content - comment.renderedContent = marked(content) - } - - if (sticky !== undefined) { - comment.sticky = sticky - } - - // 状态修改是涉及到spam修改 - if (state !== undefined) { - comment.state = state - const akismetClient = akismet.getAkismetClient() - const permalink = getPermalink(cache) - const opt = { - user_ip: cache.meta.ip, - user_agent: cache.meta.ua, - referrer: cache.meta.referer, - permalink, - comment_type: getCommentType(cache.type), - comment_author: cache.author.github.login, - comment_author_email: cache.author.github.email, - comment_author_url: cache.author.github.blog, - comment_content: cache.content, - is_test: isProd - } - - if (cache.state === -2 && state !== -2) { - // 垃圾评论转为正常评论 - if (cache.spam) { - comment.spam = false - // 报告给Akismet - akismetClient.submitSpam(opt) - } - } else if (cache.state !== -2 && state === -2) { - // 正常评论转为垃圾评论 - if (!cache.spam) { - comment.spam = true - // 报告给Akismet - akismetClient.submitHam(opt) - } - } - } - - let p = commentProxy.updateById(id, comment) - if (!ctx._isAuthenticated) { - p = p.select('-content -state -updatedAt') - .populate({ - path: 'author', - select: 'github' - }) - .populate({ - path: 'parent', - select: 'author meta sticky ups' - }) - .populate({ - path: 'forward', - select: 'author meta sticky ups' - }) - } - const data = await p.exec() - data - ? ctx.success(data, '评论更新成功') - : ctx.fail('评论更新失败') -} - -// 删除评论 -exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少评论ID').toString().isObjectId().val() - const data = await commentProxy.delById(id).exec() - - data && data.result && data.result.ok - ? ctx.success(null, '评论删除成功') - : ctx.fail('评论删除失败') -} - -// 评论点赞 -exports.like = async (ctx, next) => { - const id = ctx.validateParam('id').required('评论ID参数错误').toString().isObjectId('评论ID参数错误').val() - const like = ctx.validateBody('like').defaultTo(true).toBoolean().val() - const user = ctx.validateBody('user').optional().isObjectId().val() - let userCache = null - if (user) { - userCache = await userProxy.getById(user).exec().catch(err => { - ctx.log.error(err.message) - return null - }) - } - - let data = null - if (!userCache || !!userCache.role) { - data = await commentProxy.likeAndNotify(id, like, userCache) - } else { - data = await commentProxy.updateById(id, { - $inc: { - ups: like ? 1 : -1 - } - }).exec() - } - - data - ? ctx.success(null, '评论点赞成功') - : ctx.fail('评论点赞失败') -} - -// 获取永久链接 -function getPermalink (comment = {}) { - const { type, article } = comment - switch (type) { - case 0: - return `${config.site}/blog/article/${article}` - case 1: - return `${config.site}/guestbook` - default: - return '' - } -} - -// 评论类型说明 -function getCommentType (type) { - switch (type) { - case 0: - return '文章评论' - case 1: - return '站点留言' - default: - return '评论' - } -} - -// 检测用户以往spam评论 -async function checkUserSpam (user) { - const userComments = await commentProxy.find({ author: user._id }) - .exec() - .catch(err => { - debug.error('用户历史评论获取失败,错误:', err.message) - return [] - }) - - const spamComments = userComments.filter(c => c.spam) - // 如果用户以往评论中spam评论数量大于等于spam限制 - if (spamComments.length >= config.limit.commentSpamLimit) { - if (!user.mute) { - // 将用户禁言 - await userProxy.muteByIdAndNotify(user._id) - .then(() => debug.success('用户禁言成功,用户:', user.name)) - .catch(err => debug.error('用户禁言失败,请手动禁言,错误:', err.message)) - } - return false - } - return true -} - -// 更新文章的meta.comments评论数量 -async function updateArticleCommentCount (articleIds = []) { - if (!articleIds.length) { - return - } - // TIP: 这里必须$in的是一个ObjectId对象数组,而不能只是id字符串数组 - articleIds = [...new Set(articleIds)].filter(id => isObjectId(id)).map(id => createObjectId(id)) - const counts = await commentProxy.aggregate([ - { $match: { state: 1, article: { $in: articleIds } } }, - { $group: { _id: '$article', total_count: { $sum: 1 } } } - ]) - .exec() - .catch(err => { - debug.error('更新文章评论数量前聚合评论数据操作失败,错误:', err.message) - return [] - }) - Promise.all( - counts.map(count => articleProxy.updateById(count._id, { $set: { 'meta.comments': count.total_count } }).exec()) - ) - .then(() => debug.success('文章评论数量更新成功')) - .catch(err => debug.error('文章评论数量更新失败,错误:', err.message)) -} - -// 发送邮件 -async function sendEmailToAdminAndUser (comment, permalink) { - const { type, article } = comment - let adminTitle = '位置的评论' - let adminType = '评论' - if (type === 0) { - // 文章评论 - const at = await articleProxy.getById(article).exec().catch(() => null) - if (at && at._id) { - adminTitle = `博客文章 [${at.title}] 有了新的评论` - } - adminType = '评论' - } else if (type === 1) { - // 站内留言 - adminTitle = `个人站点有新的留言` - adminType = '留言' - } - - // 发送给管理员邮箱config.email - mailer.send({ - subject: adminTitle, - text: `来自 ${comment.author.github.name} 的${adminType}:${comment.content}`, - html: `

来自 ${comment.author.github.name} 的${adminType} [ 点击查看 ]:${comment.renderedContent}

` - }, true) - - // 发送给被评论者 - if (comment.forward) { - const forwardAuthor = await userProxy.getById(comment.forward.author).exec().catch(() => null) - if (forwardAuthor) { - mailer.send({ - to: forwardAuthor.github.email, - subject: '你在 Jooger 的博客的评论有了新的回复', - text: `来自 ${comment.author.name} 的回复:${comment.content}`, - html: `

来自 ${comment.author.name} 的回复 [ 点击查看 ]:${comment.renderedContent}

` - }) - } else { - debug.warn('给被评论者邮件失败') - } - } -} - -// 验证作者 -async function checkAuthor (author) { - let user = null - if (isObjectId(author)) { - user = await findUser({ - _id: author - }) - } else if (isType(author, 'Object')) { - // 需要创建或更新用户 - const update = {} - author.name && (update.name = author.name) - author.site && (update.site = author.site) - if (author.email) { - update.avatar = gravatar(author.email) - update.email = author.email - } - if (author.id) { - // 更新 - if (isObjectId(author.id)) { - user = await userProxy.updateByIdAndNotify(author.id, update) - .catch(err => { - debug.error('用户更新失败,错误:', err.message) - this.log.error(err.message) - return null - }) - if (user) { - debug.success(`用户【${user.name}】更新成功`) - } - } - } else { - // 创建 - user = await userProxy.createAndNotify({ - ...update, - role: config.constant.roleMap.USER - }).catch(err => { - debug.error('用户创建失败,错误:', err.message) - this.log.error(err.message) - return null - }) - if (user) { - debug.success(`用户【${user.name}】创建成功`) - } - } - } - return user -} - -async function findUser (query = {}, update) { - const user = await userProxy.findOne(query).select('-password').exec().catch(err => { - debug.error('用户查找失败,错误:', err.message) - return null - }) - return user -} diff --git a/server/controller/index.js b/server/controller/index.js deleted file mode 100644 index 6420112..0000000 --- a/server/controller/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @desc Controllers entry - * @author Jooger - * @date 26 Sep 2017 - */ - -'use strict' - -exports.article = require('./article') -exports.category = require('./category') -exports.tag = require('./tag') -exports.comment = require('./comment') -exports.music = require('./music') -exports.option = require('./option') -exports.user = require('./user') -exports.auth = require('./auth') -exports.moment = require('./moment') -exports.statistics = require('./statistics') -exports.aliyun = require('./aliyun') -exports.notification = require('./notification') diff --git a/server/controller/moment.js b/server/controller/moment.js deleted file mode 100644 index 42bc531..0000000 --- a/server/controller/moment.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * @desc Moment controller - * @author Jooger - * @date 30 Oct 2017 - */ - -'use strict' - -const config = require('../config') -const { momentProxy } = require('../proxy') -const { getLocation, getDocsPaginationData } = require('../util') - -// 动态列表 -exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(config.limit.momentLimit).toInt().gt(0, '每页数量必须大于0').val() - const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, '页码参数必须大于0').val() - const state = ctx.validateQuery('state').optional().toInt().isIn([0, 1], 'state参数错误').val() - const keyword = ctx.validateQuery('keyword').optional().toString().val() - - const query = {} - const options = { - page, - limit: pageSize, - sort: { createdAt: -1 } - } - - if (!ctx._isAuthenticated) { - query.state = 1 - } else { - if (state !== undefined) { - query.state = state - } - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { content: keywordReg } - ] - } - } - - const moments = await momentProxy.paginate(query, options) - - moments - ? ctx.success(getDocsPaginationData(moments), '动态列表获取成功') - : ctx.fail('动态列表获取失败') -} - -// 创建动态 -exports.create = async (ctx, next) => { - const content = ctx.validateBody('content').required('缺少内容').notEmpty().val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数无效').val() - const req = ctx.req - const moment = {} - const { ip, location } = getLocation(req) - - if (state !== undefined) { - moment.state = state - } - moment.location = { ip, ...location } - moment.content = content - - const data = await momentProxy.newAndSave(moment) - - data && data.length - ? ctx.success(data, '动态创建成功') - : ctx.fail('动态创建失败') -} - -// 动态更新 -exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少动态ID').toString().isObjectId().val() - const content = ctx.validateBody('content').optional().toString().val() - const state = ctx.validateBody('state').optional().toInt().isIn([0, 1], 'state参数无效').val() - const moment = {} - - if (state !== undefined) { - moment.state = state - } - content && (moment.content = content) - const data = await momentProxy.updateById(id, moment).exec() - - data - ? ctx.success(data, '动态更新成功') - : ctx.fail('动态更新失败') -} - -// 删除动态 -exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少动态ID').toString().isObjectId().val() - const data = await momentProxy.delById(id).exec() - - data && data.result && data.result.ok - ? ctx.success(null, '动态删除成功') - : ctx.fail('动态删除失败') -} diff --git a/server/controller/music.js b/server/controller/music.js deleted file mode 100644 index 12eae32..0000000 --- a/server/controller/music.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @desc Music controller - * @author Jooger - * @date 26 Sep 2017 - */ - -'use strict' - -const config = require('../config') -const { modelUpdate, netease } = require('../service') -const { optionProxy } = require('../proxy') -const { proxy } = require('../util') -const { redis } = require('../plugins') -const isProd = process.env.NODE_ENV === 'production' - -exports.list = async (ctx, next) => { - // 后台实时获取 - if (ctx._isAuthenticated) { - const playListId = ctx.validateQuery('play_list_id').required('缺少歌单ID').notEmpty().val() - - const data = await modelUpdate.fetchSonglist(playListId) - ctx.success(data) - } else { - const option = await optionProxy.findOne().exec().catch(err => { - ctx.log.error(err.message) - return null - }) - - if (!option || !option.musicId) { - return ctx.fail('歌单未找到') - } - - const playListId = option.musicId - const musicData = await redis.get(config.constant.redisCacheKey.music) - - // hit - if (musicData && musicData.id === playListId) { - return ctx.success(musicData.data || []) - } - - // update cache - const data = await modelUpdate.updateMusicCache(playListId) - ctx.success(data ? data.data : {}) - } -} - -exports.item = async (ctx, next) => { - const songId = ctx.validateParam('song_id') - .required('the "song_id" parameter is required') - .notEmpty() - .isString('the "song_id" parameter should be String type') - .val() - - const { songs } = await netease.neteaseMusic.song(songId) - - ctx.success(songs) -} - -exports.url = async (ctx, next) => { - const songId = ctx.validateParam('song_id').required('缺少歌曲ID').notEmpty().val() - - const data = await netease.neteaseMusic.url(songId).then(data => { - if (!isProd) { - return data.data || [] - } - if (Array.isArray(data.data)) { - return data.data.map(item => { - item.url = proxy(item.url) - return item - }) - } - return [] - }) - - ctx.success(data) -} - -exports.lyric = async (ctx, next) => { - const songId = ctx.validateParam('song_id').required('缺少歌曲ID').notEmpty().val() - const data = await netease.neteaseMusic.lyric(songId) - ctx.success(data) -} - -exports.cover = async (ctx, next) => { - const coverId = ctx.validateParam('cover_id').required('缺少封面ID').notEmpty().val() - const data = await netease.neteaseMusic.picture(coverId) - ctx.success(data) -} diff --git a/server/controller/notification.js b/server/controller/notification.js deleted file mode 100644 index f2d9904..0000000 --- a/server/controller/notification.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @desc Notification controller - * @author Jooger - * @date 12 Feb 2018 - */ - -'use strict' - -const config = require('../config') -const { notificationProxy } = require('../proxy') -const { getDocsPaginationData } = require('../util') -const { typeMap, categoryMap } = config.constant.notification - -// 通知列表 -exports.list = async (ctx, next) => { - const pageSize = ctx.validateQuery('per_page').defaultTo(20).toInt().gt(0, 'per_page参数必须大于0').val() - const page = ctx.validateQuery('page').defaultTo(1).toInt().gt(0, 'page参数必须大于0').val() - const type = ctx.validateQuery('type').optional().toInt().isIn(Object.values(typeMap), 'type参数错误').val() - const category = ctx.validateQuery('category').optional().toInt().isIn(Object.values(categoryMap), 'category参数错误').val() - const viewed = ctx.validateQuery('viewed').optional().toString().val() - - // 过滤条件 - const options = { - sort: { - createdAt: -1 - }, - page, - limit: pageSize, - populate: [ - { - path: 'user', - select: 'name email site' - }, - { - path: 'article', - select: 'title permalink' - } - ] - } - - // 查询条件 - const query = {} - - if (type !== undefined) { - query.type = type - } - - if (category !== undefined) { - query.category = category - } - - if (viewed !== undefined) { - query.viewed = viewed === 'true' - } - - const ns = await notificationProxy.paginate(query, options) - - ns - ? ctx.success(getDocsPaginationData(ns), '通知列表获取成功') - : ctx.fail('通知列表获取失败') -} - -// 未读通知数量 -exports.count = async (ctx, next) => { - const data = await notificationProxy.count({ viewed: false }).exec() - - data - ? ctx.success(data, '未读通知数量获取成功') - : ctx.fail('未读通知数量获取失败') -} - -// 通知已读 -exports.view = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少通知ID').toString().isObjectId().val() - const data = await notificationProxy.updateById(id, { - viewed: true - }).exec() - - data - ? ctx.success(null, '通知标记已读成功') - : ctx.fail('通知标记已读失败') -} - -// 通知已读 -exports.viewAll = async (ctx, next) => { - const data = await notificationProxy.updateMany({ viewed: false }, { viewed: true }).exec() - data && data.ok - ? ctx.success(null, '通知全部标记已读成功') - : ctx.fail('通知全部标记已读失败') -} - -// 删除通知 -exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少通知ID').toString().isObjectId().val() - const data = await notificationProxy.delById(id).exec() - - data && data.result && data.result.ok - ? ctx.success(null, '通知删除成功') - : ctx.fail('通知删除失败') -} diff --git a/server/controller/option.js b/server/controller/option.js deleted file mode 100644 index bf66bfb..0000000 --- a/server/controller/option.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @desc Option controller - * @author Jooger - * @date 26 Sep 2017 - */ - -'use strict' - -const { optionProxy } = require('../proxy') -const { modelUpdate } = require('../service') - -// 站点参数数据 -exports.data = async (ctx, next) => { - const data = await optionProxy.findOne().exec() - data - ? ctx.success(data, '站点参数获取成功') - : ctx.fail('站点参数获取失败') -} - -// 站点参数更新 -exports.update = async (ctx, next) => { - const option = ctx.request.body - const data = await modelUpdate.updateOption(option) - data - ? ctx.success(data, '站点参数更新成功') - : ctx.fail('站点参数更新失败') -} diff --git a/server/controller/statistics.js b/server/controller/statistics.js deleted file mode 100644 index eaa4af9..0000000 --- a/server/controller/statistics.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @desc Statistics controller - * @author Jooger - * @date 2 Feb 2018 - */ - -const { articleProxy, categoryProxy, tagProxy, userProxy, commentProxy } = require('../proxy') - -// OPTIMIZE: 站内统计 -exports.data = async (ctx, next) => { - const data = await Promise.all([ - articleProxy.count({ state: 1 }).exec(), - categoryProxy.count().exec(), - tagProxy.count().exec(), - userProxy.count().nor({ role: 0 }).exec(), - commentProxy.count({ type: 0 }).exec(), - commentProxy.count({ type: 1 }).exec() - ]).then(([articles, categories, tags, users, comments, guestComments]) => { - return { - article: articles, - category: categories, - tag: tags, - user: users, - comment: comments, - guestComment: guestComments - } - }) - data - ? ctx.success(data, '统计信息获取成功') - : ctx.fail('统计信息获取失败') -} diff --git a/server/controller/tag.js b/server/controller/tag.js deleted file mode 100644 index 2383cc7..0000000 --- a/server/controller/tag.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @desc Tag controller - * @author Jooger - * @date 26 Sep 2017 - */ - -'use strict' - -const { tagProxy, articleProxy } = require('../proxy') - -// 标签列表 -exports.list = async (ctx, next) => { - const keyword = ctx.validateQuery('keyword').optional().toString().val() - - const query = {} - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { name: keywordReg } - ] - } - - const data = await tagProxy.find(query).sort('-createdAt') - - if (data) { - for (let i = 0; i < data.length; i++) { - if (typeof data[i].toObject === 'function') { - data[i] = data[i].toObject() - } - const articles = await articleProxy.find({ tag: data[i]._id, state: 1 }).exec().catch(err => { - ctx.log.error(err.message) - return [] - }) - data[i].count = articles.length - } - ctx.success(data, '标签列表获取成功') - } else { - ctx.fail('标签列表获取失败') - } -} - -// 标签详情 -exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() - - let data = await tagProxy.getById(id).exec() - - if (data) { - data = data.toObject() - const articles = await articleProxy.find({ tag: id }) - .select('-tag') - .exec() - .catch(err => { - ctx.log.error(err.message) - return [] - }) - data.articles = articles - data.articles_count = articles.length - ctx.success(data, '标签详情获取成功') - } else { - ctx.fail('标签详情获取失败') - } -} - -// 标签创建 -exports.create = async (ctx, next) => { - const name = ctx.validateBody('name').required('缺少标签名称').notEmpty().val() - const description = ctx.validateBody('description').optional().val() - const ext = ctx.validateBody('extends').optional().toArray().val() - - const { length } = await tagProxy.find({ name }).exec().catch(err => { - ctx.log.error(err.message) - return [] - }) - - if (!length) { - const data = await tagProxy.newAndSave({ - name, - description, - extends: ext - }) - - data && data.length - ? ctx.success(data[0], '标签创建成功') - : ctx.fail('标签创建失败') - } else { - ctx.fail(`【${name}】标签已经存在`) - } -} - -// 标签更新 -exports.update = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() - const name = ctx.validateBody('name').optional().val() - const description = ctx.validateBody('description').optional().val() - const ext = ctx.validateBody('extends').optional().toArray().val() - const tag = {} - - name && (tag.name = name) - description && (tag.description = description) - ext && (tag.extends = ext) - - const data = await tagProxy.updateById(id, tag).exec() - - data - ? ctx.success(data, '标签更新成功') - : ctx.fail('标签更新失败') -} - -// 标签删除 -exports.delete = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少标签ID').toString().isObjectId().val() - const articles = await tagProxy.find({ tag: id }).exec() - - if (articles && articles.length) { - // 标签下面有文章,不能删除 - ctx.fail('该标签下有文章,不能删除') - } else { - const data = await tagProxy.delById(id).exec() - data && data.result && data.result.ok - ? ctx.success(null, '标签删除成功') - : ctx.fail('标签删除失败') - } -} diff --git a/server/controller/user.js b/server/controller/user.js deleted file mode 100644 index 349801e..0000000 --- a/server/controller/user.js +++ /dev/null @@ -1,159 +0,0 @@ -/** - * @desc User controlelr - * @author Jooger - * @date 26 Sep 2017 - */ - -'use strict' - -const config = require('../config') -const { userProxy, commentProxy } = require('../proxy') -const { bhash, bcompare } = require('../util') - -// 用户列表 -exports.list = async (ctx, next) => { - let select = '-password' - - if (!ctx._isAuthenticated) { - select += ' -createdAt -updatedAt -role' - } - - const data = await userProxy.find() - .sort('-createdAt') - .select(select) - - data - ? ctx.success(data, '用户列表获取成功') - : ctx.fail('用户列表获取失败') -} - -// 用户详情 -exports.item = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少用户ID').toString().isObjectId().val() - let select = '-password' - - if (!ctx._isAuthenticated) { - select += ' -createdAt -updatedAt -github' - } - - const data = await userProxy.getById(id) - .select(select) - .exec() - - data - ? ctx.success(data, '用户详情获取成功') - : ctx.fail('用户详情获取失败') -} - -// 用户更新,只能更新自己 -exports.updateMe = async (ctx, next) => { - const name = ctx.validateBody('name').optional().val() - const email = ctx.validateBody('email').optional().isEmail('Email格式错误').val() - const site = ctx.validateBody('site').optional().val() - const description = ctx.validateBody('description').optional().val() - const avatar = ctx.validateBody('avatar').optional().val() - const slogan = ctx.validateBody('slogan').optional().val() - const company = ctx.validateBody('company').optional().val() - const location = ctx.validateBody('location').optional().val() - const user = {} - - name && (user.name = name) - slogan && (user.slogan = slogan) - company && (user.company = company) - location && (user.location = location) - site && (user.site = site) - description && (user.description = description) - avatar && (user.avatar = avatar) - email && (user.email = email) - - const data = await userProxy.updateById(ctx._user._id, user).exec() - data - ? ctx.success(data, '用户更新成功') - : ctx.fail('用户更新失败') -} - -// 更新密码 -exports.password = async (ctx, next) => { - const password = ctx.validateBody('password').required('缺少新密码').notEmpty().val() - const oldPassword = ctx.validateBody('old_password').required('缺少原密码').notEmpty().val() - const vertifyPassword = bcompare(oldPassword, ctx._user.password) - if (!vertifyPassword) { - return ctx.fail('原密码错误') - } - - const data = await userProxy.updateById(ctx._user._id, { - password: bhash(password) - }).exec() - data - ? ctx.success(data, '密码更新成功') - : ctx.fail('密码更新失败') -} - -// 用户禁言/解禁 -exports.mute = async (ctx, next) => { - const id = ctx.validateParam('id').required('缺少用户ID').toString().isObjectId().val() - const mute = ctx.validateBody('mute').defaultTo(true).toBoolean().val() - const user = userProxy.getById(id).exec() - if (user && !user.role) { - return ctx.fail('管理员不能禁言') - } - const data = await userProxy.updateById(id, { mute }).exec() - const msg = mute ? '用户禁言' : '用户解禁' - data - ? ctx.success(null, `${msg}成功`) - : ctx.fail(`${msg}失败`) -} - -// 博主信息获取 -exports.blogger = async (ctx, next) => { - const data = await userProxy - .findOne({ 'github.login': config.author, role: 0 }) - .select('-password -role -createdAt -updatedAt -github -mute') - .exec() - - data - ? ctx.success(data, '博主详情获取成功') - : ctx.fail('博主详情获取失败') -} - -// 站内留言墙的用户,只限站内留言 -exports.guests = async (ctx, next) => { - // OPTIMIZE: $lookup尝试失败,只能循环查询用户了 - let data = await commentProxy.aggregate([ - { - $match: { - spam: false, // 非垃圾留言 - state: 1, // 审核通过 - type: 1 // 站内留言 - } - }, - { - $sort: { - createdAt: -1 - } - }, - { - $group: { - _id: '$author' - } - } - ]).exec() - - let list = await Promise.all((data || []).map(item => { - return userProxy.findOne({ - _id: item._id, - $nor: [ - { - role: config.constant.roleMap.ADMIN - }, { - 'github.login': config.author - } - ] - }).select('name site avatar').exec() - })) - list = list.filter(item => !!item) - ctx.success({ - list, - total: list.length - }, '站内留言用户列表获取成功') -} diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js deleted file mode 100644 index ab94783..0000000 --- a/server/middleware/authenticate.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @desc Auth middleware - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const compose = require('koa-compose') -const jwt = require('jsonwebtoken') -const config = require('../config') -const { userProxy } = require('../proxy') -const debug = require('../util').getDebug('Auth') -const redirectReg = /auth\/github\/login(.*?)/ - -// 验证本地登录token -function verifyToken () { - return async (ctx, next) => { - ctx.session._verify = false - const token = ctx.cookies.get(config.auth.session.key) - - if (token) { - let decodedToken = null - try { - decodedToken = await jwt.verify(token, config.auth.secrets) - } catch (err) { - debug.error('本地登录Token校验出错,错误:', err.message) - if (redirectReg.test(ctx.originalUrl)) { - return ctx.redirect(ctx.query.redirectUrl || config.site) - } - return ctx.fail(401) - } - - if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { - // 已校验权限 - ctx.session._verify = true - ctx.session._token = token - debug.success('本地登录Token校验成功') - } - } - await next() - } -} - -// 本地登录验证 -exports.isAuthenticated = () => { - return compose([ - verifyToken(), - async (ctx, next) => { - if (!ctx.session._verify) { - return ctx.fail(401) - } - - const userId = ctx.cookies.get(config.auth.userCookieKey, { signed: false }) - const user = await userProxy.getById(userId).exec().catch(err => { - debug.error('token验证时用户查找失败, 错误:', err.message) - ctx.log.error(err.message) - return null - }) - if (!user) { - return ctx.fail(401, '用户不存在') - } - ctx._user = user.toObject() - ctx._isAuthenticated = true - await next() - } - ]) -} diff --git a/server/middleware/error.js b/server/middleware/error.js deleted file mode 100644 index 26ba1a6..0000000 --- a/server/middleware/error.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @desc Error monitor - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -module.exports = async (ctx, next) => { - try { - await next() - } catch (err) { - let code = err.status || 500 - if (err.name === 'ValidationError') { - code = 10001 - } - ctx.fail(code, err.message) - ctx.status = 200 - if (code === 500) { - // TODO: 错误日志记录 - ctx.log.error( - { req: ctx.req, err }, - ' --> %s %s %d', - ctx.request.method, - ctx.request.originalUrl, - ctx.status - ) - } - } -} diff --git a/server/middleware/formidable.js b/server/middleware/formidable.js deleted file mode 100644 index 57795d3..0000000 --- a/server/middleware/formidable.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @desc Formidable 上传中间件,暂未使用 - * @author Jooger - * @date 10 Oct 2017 - */ - -'use strict' - -const formidable = require('formidable') - -const middleware = (opts = {}) => async (ctx, next) => { - const res = await middleware.parse(opts, ctx).catch(err => { - ctx.log.error(err.message) - return null - }) - if (res) { - ctx.request.body = res.fields - ctx.request.files = res.files - } - await next() -} - -middleware.parse = (opts = {}, ctx) => { - return new Promise((resolve, reject) => { - const form = new formidable.IncomingForm() - for (const key in opts) { - form[key] = opts[key] - } - form.parse(ctx.request, (err, fields, files) => { - if (err) return reject(err) - resolve({ - fields, - files - }) - }) - }) -} - -module.exports = middleware diff --git a/server/middleware/header.js b/server/middleware/header.js deleted file mode 100644 index a6bbea4..0000000 --- a/server/middleware/header.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @desc 设置相应头 - * @author Jooger - * @date 26 Sep 2017 - */ - -'use strict' - -const config = require('../config') - -module.exports = async (ctx, next) => { - const { request, response } = ctx - const allowedOrigins = config.constant.allowedOrigins - const origin = request.get('origin') || '' - const allowed = request.query._DEV_ || - origin.includes('localhost') || - origin.includes('127.0.0.1') || - allowedOrigins.find(item => origin.includes(item)) - if (allowed) { - response.set('Access-Control-Allow-Origin', origin) - } - response.set('Access-Control-Allow-Headers', 'token, Authorization, Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With') - response.set('Access-Control-Allow-Methods', 'PUT,PATCH,POST,GET,DELETE,OPTIONS') - response.set('Access-Control-Allow-Credentials', true) - response.set('Content-Type', 'application/json;charset=utf-8') - response.set('X-Powered-By', `${config.name}/${config.version}`) - - if (request.method === 'OPTIONS') { - return ctx.success('ok') - } - await next() -} diff --git a/server/middleware/index.js b/server/middleware/index.js deleted file mode 100644 index 589f382..0000000 --- a/server/middleware/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @desc Middleware Entry - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -exports.error = require('./error') -exports.response = require('./response') -exports.authenticate = require('./authenticate') -exports.header = require('./header') -exports.formidable = require('./formidable') diff --git a/server/middleware/response.js b/server/middleware/response.js deleted file mode 100644 index c45cb08..0000000 --- a/server/middleware/response.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @desc Reponse middleware - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const config = require('../config') -const { isType } = require('../util') -const successMsg = config.constant.codeMap['200'] -const failMsg = config.constant.codeMap['-1'] - -module.exports = async (ctx, next) => { - ctx.success = (data = null, message = successMsg) => { - ctx.status = 200 - ctx.body = { - code: 200, - success: true, - message, - data - } - } - - ctx.fail = (code = -1, message = '', data = null) => { - if (isType(code, 'String')) { - data = message || null - message = code - code = -1 - } - ctx.status = 200 - ctx.body = { - code, - success: false, - message: message || config.constant.codeMap[code] || failMsg, - data - } - } - - await next() -} diff --git a/server/model/index.js b/server/model/index.js deleted file mode 100644 index d9842e8..0000000 --- a/server/model/index.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @desc Models entry - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const mongoose = require('mongoose') -const schemas = require('./schema') -const { firstUpperCase } = require('../util') -const models = {} - -Object.keys(schemas).forEach(key => { - const schema = getSchema(schemas[key]) - if (schema) { - models[`${firstUpperCase(key)}Model`] = mongoose.model(firstUpperCase(key), schema) - } -}) - -// 构建schema -function getSchema (schema) { - if (!schema) { - return null - } - schema.set('versionKey', false) - schema.set('toObject', { getters: true }) - schema.set('toJSON', { getters: true, virtuals: false }) - schema.pre('findOneAndUpdate', updateHook) - return schema -} - -// 更新updatedAt -function updateHook (next) { - this.findOneAndUpdate({}, { updatedAt: Date.now() }) - next() -} - -module.exports = models diff --git a/server/model/schema/article.js b/server/model/schema/article.js deleted file mode 100644 index 36c06ee..0000000 --- a/server/model/schema/article.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @desc Article Model - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate') - -const articleSchema = new mongoose.Schema({ - // 文章标题 - title: { type: String, required: true }, - // 文章关键字(FOR SEO) - keywords: [{ type: String }], - // 文章摘要 (FOR SEO) - description: { type: String, default: '' }, - // 文章原始markdown内容 - content: { type: String, required: true, validate: /\S+/ }, - // markdown渲染后的htmln内容 - renderedContent: { type: String, required: true, validate: /\S+/ }, - // 分类 - category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' }, - // 标签 - tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], - // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) - thumb: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, - // 文章状态 ( 0 草稿 | 1 已发布 ) - state: { type: Number, default: 0 }, - // 永久链接 - permalink: { type: String, validate: /\S+/ }, - // 创建日期 - createdAt: { type: Date, default: Date.now }, - // 更新日期 - updatedAt: { type: Date, default: Date.now }, - // 发布日期 - publishedAt: { type: Date, default: Date.now }, - // 文章元数据 (浏览量, 喜欢数, 评论数) - meta: { - pvs: { type: Number, default: 0, validate: /^\d*$/ }, - ups: { type: Number, default: 0, validate: /^\d*$/ }, - comments: { type: Number, default: 0, validate: /^\d*$/ } - } -}) - -articleSchema.plugin(mongoosePaginate) - -module.exports = articleSchema diff --git a/server/model/schema/category.js b/server/model/schema/category.js deleted file mode 100644 index 8ced579..0000000 --- a/server/model/schema/category.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @desc Category - * @author Jooger - * @date 26 Oct 2017 - */ - -'use strict' - -const mongoose = require('mongoose') - -const categorySchema = new mongoose.Schema({ - name: { type: String, required: true }, - description: { type: String, default: '' }, - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, - // 排序 首页分类展示顺序 - list: { type: Number, default: 1 }, - extends: [{ - key: { type: String, validate: /\S+/ }, - value: { type: String, validate: /\S+/ } - }] -}) - -module.exports = categorySchema diff --git a/server/model/schema/comment.js b/server/model/schema/comment.js deleted file mode 100644 index a8ec26d..0000000 --- a/server/model/schema/comment.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @desc Comment Model - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate') - -const commentSchema = new mongoose.Schema({ - // 评论通用项 - createdAt: { type: Date, default: Date.now }, // 创建时间 - updatedAt: { type: Date, default: Date.now }, // 修改时间 - content: { type: String, required: true, validate: /\S+/ }, // 评论内容 - renderedContent: { type: String, required: true, validate: /\S+/ }, // marked渲染后的内容 - state: { type: Number, default: 1 }, // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 - spam: { type: Boolean, default: false }, // Akismet判定是否是垃圾评论,方便后台check - author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, - ups: { type: Number, default: 0, validate: /^\d*$/ }, // 点赞数 - sticky: { type: Number, default: 0 }, // 是否置顶 0 否 | 1 是 - type: { type: Number, default: 0 }, // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) - meta: { - ip: String, // 用户IP - location: Object, // IP所在地 - ua: { type: String, validate: /\S+/ }, // user agent - referer: { type: String, default: '' } - }, - // type为0时此项存在 - article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, - // 子评论具备项 - parent: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, // 父评论 parent和forward二者必须同时存在 - forward: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' } // 前一条评论ID,可以是parent的id, 比如 B评论 是 A评论的回复,则B.forward._id = A._id,主要是为了查看评论对话时的评论树构建 -}) - -commentSchema.plugin(mongoosePaginate) - -module.exports = commentSchema diff --git a/server/model/schema/index.js b/server/model/schema/index.js deleted file mode 100644 index 1d50d2e..0000000 --- a/server/model/schema/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @desc Schemas entry - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -exports.article = require('./article') -exports.comment = require('./comment') -exports.category = require('./category') -exports.tag = require('./tag') -exports.user = require('./user') -exports.option = require('./option') -exports.moment = require('./moment') -exports.notification = require('./notification') diff --git a/server/model/schema/moment.js b/server/model/schema/moment.js deleted file mode 100644 index 8619221..0000000 --- a/server/model/schema/moment.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @desc 个人动态 Model - * @author Jooger - * @date 30 Oct 2017 - */ - -'use strict' - -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate') - -const momentSchema = new mongoose.Schema({ - content: { type: String, required: true, validate: /\S+/ }, - location: { type: Object, required: true }, - state: { type: Number, default: 1 }, // 状态 0 未发布 | 1 发布 - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now } -}) - -momentSchema.plugin(mongoosePaginate) - -module.exports = momentSchema diff --git a/server/model/schema/notification.js b/server/model/schema/notification.js deleted file mode 100644 index 90d7a92..0000000 --- a/server/model/schema/notification.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @desc Notification - * @author Jooger - * @date 12 Feb 2018 - */ - -'use strict' - -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate') -const config = require('../../config') - -const notificationSchema = new mongoose.Schema({ - // 通知类型 0 系统通知 | 1 评论通知 | 2 点赞通知 | 3 用户操作通知 - type: { type: Number, required: true, validate: typeValidator }, - // 类型细化分类 - category: { type: String, required: true, validate: categoryValidator }, - // 是否已读 - viewed: { type: Boolean, default: false }, - // article user comment 根据情况是否包含 - article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, - user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, - comment: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, - // 必填 - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now } -}) - -function typeValidator (val) { - return Object.values(config.constant.notification.typeMap).includes(+val) -} - -function categoryValidator (val) { - return Object.values(config.constant.notification.categoryMap).includes(val + '') -} - -notificationSchema.plugin(mongoosePaginate) - -module.exports = notificationSchema diff --git a/server/model/schema/option.js b/server/model/schema/option.js deleted file mode 100644 index eb36505..0000000 --- a/server/model/schema/option.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @desc Option schema - * @author Jooger - * @date 26 Sep 2017 - */ - -'use strict' - -const mongoose = require('mongoose') - -const optionSchema = new mongoose.Schema({ - welcome: { type: String, default: '' }, - description: { type: String, default: '' }, - hobby: { type: String, default: '' }, - skill: { type: String, default: '' }, - music: { type: String, default: '' }, - location: { type: String, default: '' }, - company: { type: String, default: '' }, - links: [{ - name: { type: String, required: true }, - github: { type: String, default: '' }, - avatar: { type: String, default: '' }, - slogan: { type: String, default: '' }, - site: { type: String, required: true } - }], - musicId: { type: String, default: '' } -}) - -module.exports = optionSchema diff --git a/server/model/schema/tag.js b/server/model/schema/tag.js deleted file mode 100644 index 85116ae..0000000 --- a/server/model/schema/tag.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @desc Tag - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const mongoose = require('mongoose') - -const tagSchema = new mongoose.Schema({ - name: { type: String, required: true }, - description: { type: String, default: '' }, - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, - extends: [{ - key: { type: String, validate: /\S+/ }, - value: { type: String, validate: /\S+/ } - }] -}) - -module.exports = tagSchema diff --git a/server/model/schema/user.js b/server/model/schema/user.js deleted file mode 100644 index c30705f..0000000 --- a/server/model/schema/user.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @desc Admin schema - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const mongoose = require('mongoose') -const config = require('../../config') -const { isEmail, isSiteUrl } = require('../../util') - -const userSchema = new mongoose.Schema({ - name: { type: String, default: config.auth.defaultName, required: true }, - email: { type: String, required: true, validate: isEmail }, - avatar: { type: String, required: true }, - site: { type: String, validate: isSiteUrl }, - slogan: { type: String }, - description: { type: String, default: '' }, - // 角色 0 管理员 | 1 普通用户 | 2 github用户,不能更改 - role: { type: Number, default: 1 }, - // role = 0的时候才有该项 - password: { type: String }, - // 是否被禁言 - mute: { type: Boolean, default: false }, - company: { type: String, default: '' }, - location: { type: String, default: '' }, - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, - // github信息,不能手动更改 - github: { - id: { type: String, default: '' }, - login: { type: String, default: '' } - } -}) - -module.exports = userSchema diff --git a/server/plugins/akismet.js b/server/plugins/akismet.js deleted file mode 100644 index 05bd88f..0000000 --- a/server/plugins/akismet.js +++ /dev/null @@ -1,150 +0,0 @@ -/** - * @desc Akismet - * @author Jooger - * @date 29 Oct 2017 - */ - -'use strict' - -const akismet = require('akismet-api') -const config = require('../config') -const { getDebug } = require('../util') -const debug = getDebug('Akismet') -let akismetClient = null - -// Akismet apikey是否验证通过 -let isValidKey = false - -/** - * @desc Akismet Client Class - * @param {String} [required] key Akismet apikey - * @param {String} [required] site Akismet site - */ -class AkismetClient { - constructor (key, site) { - this.key = key - this.site = site - this.initClient() - } - - initClient () { - this.client = akismet.client({ - key: this.key, - blog: this.site - }) - } - - async verifyKey () { - let valid = true - let error = '' - if (!isValidKey) { - await this.client.verifyKey().then(v => { - valid = v - if (v) { - isValidKey = true - } else { - error = '无效的Apikey' - this.client = null - } - }).catch(err => { - error = 'Apikey验证失败,错误:' + err.message - }) - } - return { valid, client: this, error } - } - - // 检测是否是spam - checkSpam (opt = {}) { - debug.info('验证评论中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.checkSpam(opt, (err, spam) => { - if (err) { - debug.error('评论验证失败,将跳过Spam验证,错误:', err.message) - return reject(false) - } - if (spam) { - debug.warn('评论验证不通过,疑似垃圾评论') - resolve(true) - } else { - debug.success('评论验证通过') - resolve(false) - } - }) - } else { - debug.warn('Apikey未认证,将跳过Spam验证') - resolve(false) - } - }) - } - - // 提交被误检为spam的正常评论 - submitSpam (opt = {}) { - debug.info('误检Spam垃圾评论报告提交中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.submitSpam(opt, err => { - if (err) { - debug.error('误检Spam垃圾评论报告提交失败') - return reject(err) - } - debug.success('误检Spam垃圾评论报告提交成功') - resolve() - }) - } else { - debug.warn('Apikey未认证,误检Spam垃圾评论报告提交失败') - resolve() - } - }) - } - - // 提交被误检为正常评论的spam - submitHam (opt = {}) { - debug.info('误检正常评论报告提交中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.submitSpam(opt, err => { - if (err) { - debug.error('误检正常评论报告提交失败') - return reject(err) - } - debug.success('误检正常评论报告提交成功') - resolve() - }) - } else { - debug.warn('Apikey未认证,误检正常评论报告提交失败') - resolve() - } - }) - } -} - -/** - * @desc 生成Akismet clients - */ -exports.start = async () => { - const akismetConfig = config.akismet - const { apiKey } = akismetConfig - const site = config.site - const { valid, client, error } = await new AkismetClient(apiKey, site).verifyKey() - - if (valid) { - debug.success('服务启动成功') - akismetClient = client - } else { - debug.error('服务启动失败', error ? `,${error}` : '') - } -} - -/** - * @desc 根据站点地址获取对应Akismet client - * @param {String} site 站点地址 - * @return {AkismetClient} akismetClient Akismet client - */ -exports.getAkismetClient = () => { - if (!akismetClient) { - debug.warn('未找到客户端,将跳过spam验证') - return null - } - return akismetClient -} diff --git a/server/plugins/crontab.js b/server/plugins/crontab.js deleted file mode 100644 index 6915356..0000000 --- a/server/plugins/crontab.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @desc 定时任务 - * @author Jooger - * @date 27 Oct 2017 - */ - -'use strict' - -const { getDebug } = require('../util') -const { modelUpdate } = require('../service') -const debug = getDebug('Crontab') - -exports.start = () => setTimeout(() => { - // 友链 每1小时更新一次 - modelUpdate.updateOption() - setInterval(modelUpdate.updateOption, 1000 * 60 * 60 * 1) - // 用户 每1天更新一次 - modelUpdate.updateGithubInfo() - setInterval(modelUpdate.updateGithubInfo, 1000 * 60 * 60 * 24) - debug.success('定时任务启动成功') -}, 0) diff --git a/server/plugins/index.js b/server/plugins/index.js deleted file mode 100644 index d8b88d3..0000000 --- a/server/plugins/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @desc Plugins entry - * @author Jooger - * @date 29 Oct 2017 - */ - -'use strict' - -exports.mongo = require('./mongo') -exports.redis = require('./redis') -exports.akismet = require('./akismet') -exports.validation = require('./validation') -exports.mailer = require('./mailer') -exports.crontab = require('./crontab') diff --git a/server/plugins/mailer.js b/server/plugins/mailer.js deleted file mode 100644 index 26404e7..0000000 --- a/server/plugins/mailer.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @desc Mail plugin - * @author Jooger - * @date 29 Oct 2017 - */ - -'use strict' - -const nodemailer = require('nodemailer') -const config = require('../config') -const { getDebug } = require('../util') -const debug = getDebug('Mailer') -const isProd = process.env.NODE_ENV === 'production' - -let isVerify = false -const transporter = isProd ? nodemailer.createTransport({ - service: '163', - secure: true, - auth: { - user: config.email_163, - pass: process.env['163Pass'] || '163邮箱密码' - } -}) : null - -exports.start = async () => { - return new Promise((resolve, reject) => { - if (!transporter) { - return - } - transporter.verify((err, success) => { - if (err) { - isVerify = false - debug.error('服务启动失败,将在1分钟后重试,错误:', err.message) - reject(err) - setTimeout(exports.start, 60 * 1000) - } else { - isVerify = true - debug.success('服务启动成功') - resolve() - } - }) - }) -} - -/** - * @desc 发送邮件 - * @param {Object} opt={} 邮件参数 - * @param {Boolean} toMe=false 是否是给自己发送邮件 - */ -exports.send = (opt = {}, toMe = false) => { - if (!isVerify) { - return debug.warn('客户端未验证,拒绝发送邮件') - } - opt.from = `${config.author} <${config.email}>` - if (toMe) { - opt.to = config.email - } - transporter.sendMail(opt, (err, info) => { - if (err) { - return debug.error('邮件发送失败,错误:', err.message) - } - debug.success('邮件发送成功', info.messageId, info.response) - }) -} diff --git a/server/plugins/mongo.js b/server/plugins/mongo.js deleted file mode 100644 index 3a4e6ef..0000000 --- a/server/plugins/mongo.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @desc Mongodb connect - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const config = require('../config') -const mongoose = require('mongoose') -const { bhash, getDebug, proxy } = require('../util') -const { userProxy, optionProxy } = require('../proxy') -const { getGithubUsersInfo } = require('../service') -const debug = getDebug('MongoDB') -let isConnected = false - -mongoose.Promise = global.Promise - -exports.connect = () => { - mongoose.connect(config.mongo.uri, config.mongo.option).then(() => { - debug.success('连接成功') - isConnected = true - seed() - }, err => { - isConnected = false - return debug.error('连接失败,错误: ', config.mongo.uri, err.message) - }) -} - -exports.seed = seed - -function seed () { - if (isConnected) { - seedOption() - seedAdmin() - } -} - -// 参数初始化 -async function seedOption () { - const option = await optionProxy.findOne().exec().catch(err => debug.error(err.message)) - - if (!option) { - await optionProxy.newAndSave().catch(err => debug.error(err.message)) - } -} - -// 管理员初始化 -async function seedAdmin () { - const admin = await userProxy.findOne({ - role: config.constant.roleMap.ADMIN, - 'github.login': config.author - }).exec() - .catch(err => debug.error('初始化管理员查询失败,错误:', err.message)) - if (!admin) { - createAdmin() - } -} - -async function createAdmin () { - let data = await getGithubUsersInfo(config.auth.defaultName) - if (!data || !data[0]) { - return fail('未找到Github用户数据') - } - data = data[0] - const result = await userProxy.newAndSave({ - role: config.constant.roleMap.ADMIN, - name: data.name, - email: data.email, - password: bhash(config.auth.defaultPassword), - slogan: data.bio, - site: data.blog || data.url, - avatar: proxy(data.avatar_url), - company: data.company, - location: data.location, - github: { - id: data.id, - login: data.login - } - }).catch(err => fail(err.message)) - - if (!result || !result.length) { - fail('本地入库失败') - } else { - debug.success('初始化管理员成功') - } - - function fail (msg = '') { - debug.error('初始化管理员失败,错误:', msg) - } -} diff --git a/server/plugins/redis.js b/server/plugins/redis.js deleted file mode 100644 index 0c09d93..0000000 --- a/server/plugins/redis.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @desc Redis connect - * @author Jooger - * @date 27 Oct 2017 - */ - -'use strict' - -const redis = require('redis') -const config = require('../config') -const { getDebug, isType } = require('../util') -const debug = getDebug('Redis') -let client = null -let connected = false -const cache = {} - -exports.connect = () => { - if (client) { - return debug('已连接') - } - client = redis.createClient(config.redis) - client.on('error', err => { - debug.error('连接失败, 错误: ', err.message) - connected = false - }) - client.on('connect', () => { - debug.success('连接成功') - connected = true - }) - client.on('reconnecting', () => debug('正在重连中...')) -} - -// 默认 1小时 过期 -exports.set = (key = '', value = '', expired = 60 * 60) => new Promise((resolve, reject) => { - if (connected) { - if (!isType(value, 'String')) { - try { - value = JSON.stringify(value) - } catch (err) { - debug.error('存储时,序列化失败, 错误:%s', err.message) - value = value.toString() - } - } - client.set(key, value, 'EX', expired, (err, res) => { - if (err) { - debug.error('存储【 %s 】失败,错误:%s', key, err.message) - return reject(err) - } - resolve(res) - }) - } else { - cache[key] = value - resolve(value) - } -}) - -exports.get = (key = '') => new Promise((resolve, reject) => { - if (connected) { - client.get(key, (err, res) => { - if (err) { - debug.error('读取【 %s 】失败,错误:%s', key, err.message) - return reject(err) - } - try { - res = JSON.parse(res) - } catch (err) { - debug.error('获取时,序列化失败, 错误:%s', err.message) - } - resolve(res) - }) - } else { - resolve(cache[key]) - } -}) diff --git a/server/plugins/validation.js b/server/plugins/validation.js deleted file mode 100644 index a238dd2..0000000 --- a/server/plugins/validation.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @desc Custom Validations for koa-bouncer - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const mongoose = require('mongoose') -const Validator = require('koa-bouncer').Validator -const { isObjectId } = require('../util') - -Validator.addMethod('notEmpty', function (tip) { - this.isString(`${this.key}参数格式错误,期望格式:String`) - if (this.val().length === 0) { - this.throwError(tip || `${this.key}参数不能为空`) - } - return this -}) - -Validator.addMethod('isObjectId', function (tip) { - const val = this.val() - if (val !== undefined) { - this.toString() - if (!mongoose.Types.ObjectId.isValid(val)) { - this.throwError(tip || `${this.key}参数格式错误,期望格式:ObjectId`) - } - } - return this -}) - -Validator.addMethod('isObjectIdArray', function (tip) { - const val = this.val() - if (val !== undefined) { - this.isArray() - val.forEach(data => { - if (!isObjectId(data)) { - this.throwError(tip || `${this.key}参数格式错误,期望格式:[ObjectId]`) - } - }) - } - return this -}) - -module.exports = Validator diff --git a/server/proxy/article.js b/server/proxy/article.js deleted file mode 100644 index e8be41f..0000000 --- a/server/proxy/article.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @desc Article model proxy - * @author Jooger - * @date 26 Jan 2018 - */ - -'use strict' - -const BaseProxy = require('./base') -const { ArticleModel } = require('../model') -const notificationProxy = require('./notification') -const config = require('../config') -const { typeMap, categoryMap } = config.constant.notification - -class ArticleProxy extends BaseProxy { - constructor () { - super(ArticleModel) - } - - likeAndNotify (id, like, user) { - return this.updateById(id, { - $inc: { - 'meta.ups': like ? 1 : -1 - } - }).exec().then(res => { - if (res) { - const payload = { - type: typeMap.LIKE, - category: categoryMap[like ? 'LIKE_ARTICLE' : 'UNLIKE_ARTICLE'], - article: id - } - if (user) { - payload.user = typeof user === 'string' ? user : user._id - } - notificationProxy.gen(payload) - } - return res - }) - } -} - -module.exports = new ArticleProxy() diff --git a/server/proxy/base.js b/server/proxy/base.js deleted file mode 100644 index f07903f..0000000 --- a/server/proxy/base.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @desc Base model proxy - * @author Jooger - * @date 27 Jan 2018 - */ - -'use strict' - -module.exports = class BaseProxy { - constructor (Model) { - this.Model = Model - } - - newAndSave (docs) { - if (!Array.isArray(docs)) { - docs = [docs] - } - return this.Model.insertMany(docs) - } - - paginate (query, opt = {}) { - return this.Model.paginate(query, opt) - } - - getById (id) { - return this.Model.findById(id) - } - - find (query = {}, opt = {}) { - return this.Model.find(query, null, opt) - } - - findOne (query = {}, opt = {}) { - return this.Model.findOne(query, null, opt) - } - - updateById (id, doc, opt = {}) { - return this.Model.findByIdAndUpdate(id, doc, { - new: true, - ...opt - }) - } - - updateOne (query = {}, doc = {}, opt = {}) { - return this.Model.findOneAndUpdate(query, doc, { - new: true, - ...opt - }) - } - - updateMany (query = {}, doc = {}, opt = {}) { - return this.Model.update(query, doc, { - multi: true, - ...opt - }) - } - - del (query = {}) { - return this.Model.remove(query) - } - - delById (id = '') { - return this.del({ _id: id }) - } - - delByIds (ids = []) { - return this.del({ - _id: { - $in: ids - } - }) - } - - aggregate (opt = {}) { - return this.Model.aggregate(opt) - } - - count (query = {}) { - return this.Model.count(query) - } -} diff --git a/server/proxy/category.js b/server/proxy/category.js deleted file mode 100644 index bcae877..0000000 --- a/server/proxy/category.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @desc Category model proxy - * @author Jooger - * @date 27 Jan 2018 - */ - -'use strict' - -const BaseProxy = require('./base') -const { CategoryModel } = require('../model') - -class CategoryProxy extends BaseProxy { - constructor () { - super(CategoryModel) - } -} - -module.exports = new CategoryProxy() diff --git a/server/proxy/comment.js b/server/proxy/comment.js deleted file mode 100644 index 5685463..0000000 --- a/server/proxy/comment.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @desc Comment model proxy - * @author Jooger - * @date 27 Jan 2018 - */ - -'use strict' - -const BaseProxy = require('./base') -const { CommentModel } = require('../model') -const notificationProxy = require('./notification') -const config = require('../config') -const { typeMap, categoryMap } = config.constant.notification - -class CommentProxy extends BaseProxy { - constructor () { - super(CommentModel) - } - - // 生成通知的创建 - createAndNotify (model) { - return this.newAndSave(model).then(res => { - if (res && res.length) { - notificationProxy.gen({ - type: typeMap.COMMENT, - category: categoryMap[model.forward ? 'COMMENT_REPLY' : 'COMMENT_COMMENT'], - comment: res[0]._id - }) - } - return res[0] - }) - } - - likeAndNotify (id, like, user) { - return this.updateById(id, { - $inc: { - ups: like ? 1 : -1 - } - }).exec().then(res => { - if (res) { - const payload = { - type: typeMap.LIKE, - category: categoryMap[like ? 'LIKE_COMMENT' : 'UNLIKE_COMMENT'], - comment: id - } - if (user) { - payload.user = typeof user === 'string' ? user : user._id - } - notificationProxy.gen(payload) - } - return res - }) - } -} - -module.exports = new CommentProxy() diff --git a/server/proxy/index.js b/server/proxy/index.js deleted file mode 100644 index 4b8a830..0000000 --- a/server/proxy/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @desc Model proxy entrance - * @author Jooger - * @date 26 Jan 2018 - */ - -'use strict' - -module.exports = { - articleProxy: require('./article'), - categoryProxy: require('./category'), - tagProxy: require('./tag'), - userProxy: require('./user'), - commentProxy: require('./comment'), - optionProxy: require('./option'), - momentProxy: require('./moment'), - notificationProxy: require('./notification') -} diff --git a/server/proxy/moment.js b/server/proxy/moment.js deleted file mode 100644 index 32e318b..0000000 --- a/server/proxy/moment.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @desc Moment model proxy - * @author Jooger - * @date 28 Jan 2018 - */ - -'use strict' - -const BaseProxy = require('./base') -const { MomentModel } = require('../model') - -class MomentProxy extends BaseProxy { - constructor () { - super(MomentModel) - } -} - -module.exports = new MomentProxy() diff --git a/server/proxy/notification.js b/server/proxy/notification.js deleted file mode 100644 index 46dca8f..0000000 --- a/server/proxy/notification.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @desc Notification model proxy - * @author Jooger - * @date 26 Jan 2018 - */ - -'use strict' - -const BaseProxy = require('./base') -const { NotificationModel } = require('../model') -const config = require('../config') -const { getDebug } = require('../util') -const debug = getDebug('Notification') -const { typeMap, categoryMap } = config.constant.notification - -class NotificationProxy extends BaseProxy { - constructor () { - super(NotificationModel) - } - - // 生成站内消息 - gen (model) { - return this.newAndSave(model).then(res => { - if (res && res.length) { - debug('通知生成成功,', `类型[${getKeyByValue(typeMap, model.type)}],分类[${getKeyByValue(categoryMap, model.category)}],ID[${res[0]._id}]`) - } - return res - }).catch(err => { - debug.error('通知生成失败,错误:', err.message) - return null - }) - } -} - -function getKeyByValue (obj, val) { - return Object.keys(obj).find(key => val === obj[key]) -} - -module.exports = new NotificationProxy() diff --git a/server/proxy/option.js b/server/proxy/option.js deleted file mode 100644 index a3b367d..0000000 --- a/server/proxy/option.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @desc Option model proxy - * @author Jooger - * @date 28 Jan 2018 - */ - -'use strict' - -const BaseProxy = require('./base') -const { OptionModel } = require('../model') - -class OptionProxy extends BaseProxy { - constructor () { - super(OptionModel) - } -} - -module.exports = new OptionProxy() diff --git a/server/proxy/tag.js b/server/proxy/tag.js deleted file mode 100644 index 948d67a..0000000 --- a/server/proxy/tag.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @desc Tag model proxy - * @author Jooger - * @date 27 Jan 2018 - */ - -'use strict' - -const BaseProxy = require('./base') -const { TagModel } = require('../model') - -class TagProxy extends BaseProxy { - constructor () { - super(TagModel) - } -} - -module.exports = new TagProxy() diff --git a/server/proxy/user.js b/server/proxy/user.js deleted file mode 100644 index 9d21f32..0000000 --- a/server/proxy/user.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @desc User model proxy - * @author Jooger - * @date 27 Jan 2018 - */ - -'use strict' - -const BaseProxy = require('./base') -const { UserModel } = require('../model') -const notificationProxy = require('./notification') -const config = require('../config') -const { typeMap, categoryMap } = config.constant.notification - -class UserProxy extends BaseProxy { - constructor () { - super(UserModel) - } - - // 生成通知的创建 - createAndNotify (model) { - return this.newAndSave(model).then(res => { - if (res && res.length && !!res[0].role) { - // 管理员不生成通知 - notify(typeMap.USER, categoryMap.USER_CREATE, res[0]._id) - } - return res[0] - }) - } - - // 生成通知的更新 - updateByIdAndNotify (id, doc, opt = {}) { - return this.updateById(...arguments).exec().then(res => { - if (res && !!res.role) { - // 管理员不生成通知 - notify(typeMap.USER, categoryMap.USER_UPDATE, id) - } - return res - }) - } - - muteByIdAndNotify (id) { - return this.updateById(id, { mute: true }).exec().then(res => { - if (res && !!res.role) { - // 管理员不生成通知 - notify(typeMap.GENERAL, categoryMap.USER_UPDATE, id) - } - return res - }) - } -} - -function notify (type, category, user) { - if (!type || !category || !user) { - return - } - notificationProxy.gen({ - type, - category, - user - }) -} - -module.exports = new UserProxy() diff --git a/server/routes/backend.js b/server/routes/backend.js deleted file mode 100644 index 9974fbe..0000000 --- a/server/routes/backend.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @desc backend api map - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const router = require('koa-router')() -const { - article, - category, - tag, - comment, - option, - user, - auth, - music, - statistics, - moment, - aliyun, - notification -} = require('../controller') -const { authenticate } = require('../middleware') -const isAuthenticated = authenticate.isAuthenticated() - -// Article -router.get('/articles', isAuthenticated, article.list) -router.get('/articles/:id', isAuthenticated, article.item) -router.post('/articles', isAuthenticated, article.create) -router.patch('/articles/:id', isAuthenticated, article.update) -router.delete('/articles/:id', isAuthenticated, article.delete) -router.post('/articles/:id/like', isAuthenticated, article.like) - -// Comment -router.get('/comments', isAuthenticated, comment.list) -router.get('/comments/:id', isAuthenticated, comment.item) -router.post('/comments', isAuthenticated, comment.create) -router.patch('/comments/:id', isAuthenticated, comment.update) -router.delete('/comments/:id', isAuthenticated, comment.delete) -router.post('/comments/:id/like', isAuthenticated, comment.like) - -// Tag -router.get('/tags', isAuthenticated, tag.list) -router.get('/tags/:id', isAuthenticated, tag.item) -router.post('/tags', isAuthenticated, tag.create) -router.patch('/tags/:id', isAuthenticated, tag.update) -router.delete('/tags/:id', isAuthenticated, tag.delete) - -// Category -router.get('/categories', isAuthenticated, category.list) -router.get('/categories/:id', isAuthenticated, category.item) -router.post('/categories', isAuthenticated, category.create) -router.patch('/categories/:id', isAuthenticated, category.update) -router.delete('/categories/:id', isAuthenticated, category.delete) - -// Option -router.get('/options', isAuthenticated, option.data) -router.patch('/options', isAuthenticated, option.update) - -// User -router.get('/users', isAuthenticated, user.list) -router.get('/users/blogger', isAuthenticated, user.blogger) -router.get('/users/guests', isAuthenticated, user.guests) -router.get('/users/:id', isAuthenticated, user.item) -router.patch('/users/me/password', isAuthenticated, user.password) -router.patch('/users/me', isAuthenticated, user.updateMe) -router.patch('/users/:id/mute', isAuthenticated, user.mute) - -// Music -router.get('/music/songs', isAuthenticated, music.list) -router.get('/music/songs/:song_id', isAuthenticated, music.item) -router.get('/music/songs/:song_id/url', isAuthenticated, music.url) -router.get('/music/songs/:song_id/lyric', isAuthenticated, music.lyric) - -// Auth -router.get('/auth/local/logout', isAuthenticated, auth.logout) -router.post('/auth/local/login', auth.localLogin) -router.get('/auth/info', isAuthenticated, auth.info) - -// Moment -router.get('/moments', isAuthenticated, moment.list) -router.post('/moments', isAuthenticated, moment.create) -router.patch('/moments/:id', isAuthenticated, moment.update) -router.delete('/moments/:id', isAuthenticated, moment.delete) - -// Statistics -router.get('/statistics', isAuthenticated, statistics.data) - -// Aliyun OSS -router.get('/aliyun/oss', isAuthenticated, aliyun.oss) - -// Notifications -router.get('/notifications', isAuthenticated, notification.list) -router.get('/notifications/count', isAuthenticated, notification.count) -router.post('/notifications/:id/view', isAuthenticated, notification.view) -router.post('/notifications/viewall', isAuthenticated, notification.viewAll) -router.delete('/notifications/:id', isAuthenticated, notification.delete) - -module.exports = router diff --git a/server/routes/frontend.js b/server/routes/frontend.js deleted file mode 100644 index 2471ddf..0000000 --- a/server/routes/frontend.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @desc front api map - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const router = require('koa-router')() -const { - article, - category, - tag, - comment, - music, - option, - user, - auth, - moment -} = require('../controller') - -// Article -router.get('/articles', article.list) -router.get('/articles/hot', article.hot) -router.get('/articles/archives', article.archives) -router.get('/articles/:id', article.item) -router.post('/articles/:id/like', article.like) - -// Comment -router.get('/comments', comment.list) -router.get('/comments/:id', comment.item) -router.post('/comments', comment.create) -router.post('/comments/:id/like', comment.like) - -// Tag -router.get('/tags', tag.list) -router.get('/tags/:id', tag.item) - -// Category -router.get('/categories', category.list) -router.get('/categories/:id', category.item) - -// Music -router.get('/music/songs', music.list) -router.get('/music/songs/:song_id', music.item) -router.get('/music/songs/:song_id/url', music.url) -router.get('/music/songs/:song_id/lyric', music.lyric) - -// Option -router.get('/options', option.data) - -// User -router.get('/users/blogger', user.blogger) -router.get('/users/guests', user.guests) -router.get('/users/:id', user.item) - -// Auth -router.get('/auth/github/token', auth.fetchGithubToken) -router.get('/auth/github/user', auth.fetchGithubUser) - -// Moment -router.get('/moments', moment.list) - -module.exports = router diff --git a/server/routes/index.js b/server/routes/index.js deleted file mode 100644 index b0f6c2a..0000000 --- a/server/routes/index.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @desc Routes entry - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const router = require('koa-router')() -const frontend = require('./frontend') -const backend = require('./backend') -const { header } = require('../middleware') -const config = require('../config') - -module.exports = app => { - router.use('*', header) - - router.get('/', async (ctx, next) => { - ctx.body = { - name: config.name, - version: config.version, - author: config.author, - github: 'https://github.com/jo0ger', - site: config.site, - poweredBy: ['Koa2', 'MongoDB', 'Nginx'] - } - }) - - router.use('/backend', backend.routes(), backend.allowedMethods()) - router.use(frontend.routes(), frontend.allowedMethods()) - - router.all('*', (ctx, next) => { - ctx.fail(404, `${ctx.path} 不支持 ${ctx.method} 请求类型`) - }) - - app.use(router.routes(), router.allowedMethods()) -} diff --git a/server/service/github-token.js b/server/service/github-token.js deleted file mode 100644 index c06aabe..0000000 --- a/server/service/github-token.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @desc Github access_token - * @author Jooger - * @date 2 Nov 2017 - */ - -'use strict' - -const axios = require('axios') -const { getDebug } = require('../util') -const config = require('../config') -const { clientID, clientSecret } = config.sns.github -const debug = getDebug('Github:Token') - -module.exports = async code => { - const data = await axios.post('https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token', { - client_id: clientID, - client_secret: clientSecret, - code - }, { - headers: { - Accept: 'application/json', - 'X-Requested-With': 'XMLHttpRequest' - } - }).catch(err => { - debug.error('Github Token获取失败,错误:', err.message) - return null - }) - - if (data && data.data.access_token) { - return data.data - } - return null -} diff --git a/server/service/github-userinfo.js b/server/service/github-userinfo.js deleted file mode 100644 index d18f931..0000000 --- a/server/service/github-userinfo.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @desc github userinfo fetch service - * @author Jooger - * @date 27 Sep 2017 - */ - -'use strict' - -const axios = require('axios') -const { getDebug } = require('../util') -const config = require('../config') -const { clientID, clientSecret } = config.sns.github -const debug = getDebug('Github:User') - -exports.getGithubUsersInfo = (githubNames = '') => { - if (!githubNames) { - return null - } else if (typeof githubNames === 'string') { - githubNames = [githubNames] - } else if (!Array.isArray(githubNames)) { - return null - } - - const task = githubNames.map(name => { - return axios.get(`https://api.github.com/users/${name}`, { - params: { - client_id: clientID, - client_secret: clientSecret - } - }, { - headers: { - Accept: 'application/json' - } - }).then(res => { - if (res && res.status === 200) { - debug.success('【 %s 】信息抓取成功', name) - return res.data - } - return null - }).catch(err => { - debug.error('【 %s 】信息抓取失败,错误:%s', name, err.message) - return null - }) - }) - - return Promise.all(task) -} - -exports.getGithubAuthUserInfo = (access_token = '') => { - return axios.get('https://api.github.com/user', { - params: { access_token } - }).then(res => { - return res.data - }).catch(err => { - debug.error('获取用户信息失败,错误:', err.message) - return null - }) -} diff --git a/server/service/index.js b/server/service/index.js deleted file mode 100644 index da91a75..0000000 --- a/server/service/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @desc Services entry - * @author Jooger - * @date 27 Sep 2017 - */ - -'use strict' - -const { getGithubUsersInfo, getGithubAuthUserInfo } = require('./github-userinfo') - -exports.getGithubUsersInfo = getGithubUsersInfo -exports.getGithubAuthUserInfo = getGithubAuthUserInfo -exports.getGithubToken = require('./github-token') -exports.netease = require('./netease-music') -exports.modelUpdate = require('./model-update') diff --git a/server/service/model-update.js b/server/service/model-update.js deleted file mode 100644 index ecb64b8..0000000 --- a/server/service/model-update.js +++ /dev/null @@ -1,191 +0,0 @@ -/** - * @desc Models update - * @author Jooger - * @date 28 Jan 2018 - */ - -'use strict' - -const config = require('../config') -const { userProxy, optionProxy } = require('../proxy') -const { getGithubUsersInfo } = require('./github-userinfo') -const netease = require('./netease-music') -const { getDebug, proxy } = require('../util') -const debug = getDebug('ModelUpdate') -const isProd = process.env.NODE_ENV === 'production' - -// update lock -let updateOptionLock = false -exports.updateOption = async (option = null) => { - if (updateOptionLock) { - debug.warn('站点参数更新中...') - return - } - updateOptionLock = true - if (!option) { - option = await optionProxy.findOne().exec().catch(err => { - debug.error('数据查找失败,错误:', err.message) - return {} - }) - if (!option) return - } - - // 更新友链 - option.links = await generateLinks(option.links) - - const data = await optionProxy.updateOne({}, option).exec().catch(err => { - debug.error('数据更新失败,错误:', err.message) - return null - }) - - if (data) { - debug.success('站点参数更新成功') - } - updateOptionLock = false - return data -} - -// 更新github用户信息 -exports.updateGithubInfo = async () => { - const users = await userProxy.find() - .exec() - .catch(err => { - debug.error('用户查找失败,错误:', err.message) - return [] - }) - const githubUsers = users.reduce((sum, user) => { - if (user.role === config.constant.roleMap.GITHUB_USER || (user.role === config.constant.roleMap.ADMIN && user.github.login)) { - sum.push(user) - } - return sum - }, []) - const updates = await getGithubUsersInfo(githubUsers.map(user => user.github.login)) - Promise.all( - updates.reduce((tasks, data, index) => { - if (!data) return tasks - const user = githubUsers[index] - const u = { - name: data.name, - email: data.email, - avatar: proxy(data.avatar_url), - site: data.blog || data.url, - slogan: data.bio, - company: data.company, - location: data.location, - github: { - id: data.id, - login: data.login - } - } - tasks.push( - userProxy.updateById(user._id, u).exec().catch(err => { - debug.error('Github用户信息更新失败,错误:', err.message) - return null - }) - ) - return tasks - }, []) - ).then(() => { - debug.success('Github用户信息全部更新成功') - }).catch(err => { - debug.error(err.message) - }) -} - -// 获取除了歌曲链接和歌词外其他信息 -exports.fetchSonglist = playListId => { - return netease.neteaseMusic._playlist(playListId).then(({ playlist }) => { - if (!playlist) { - return null - } - const tracks = playlist.tracks.map(({ name, id, ar, al, dt, tns }) => { - return { - id, - name, - duration: dt || 0, - album: al ? { - name: al.name, - cover: isProd ? (proxy(al.picUrl) || '') : al.picUrl, - tns: al.tns - } : {}, - artists: ar ? ar.map(({ id, name }) => ({ id, name })) : [], - tns: tns || [] - } - }) - return { - id: playListId, - tracks, - name: playlist.name, - description: playlist.description, - tags: playlist.tags - } - }).catch(err => { - debug.error('歌单列表获取失败,错误:', err.message) - return null - }) -} - -// 更新song list cache -let musicCacheLock = false -exports.updateMusicCache = async function (playListId = '') { - const { redis } = require('../plugins') - if (musicCacheLock) { - debug.warn('缓存更新中...') - return redis.get(config.constant.redisCacheKey.music) || null - } - musicCacheLock = true - if (!playListId) { - const option = await optionProxy.findOne().exec().catch(err => { - debug.error('Option查找失败,错误:', err.message) - return null - }) - - if (!option || !option.musicId) { - debug.warn('歌单ID未配置') - musicCacheLock = false - return redis.get(config.constant.redisCacheKey.music) || null - } - playListId = option.musicId - } - - const data = await exports.fetchSonglist(playListId) - if (!data) { - musicCacheLock = false - return redis.get(config.constant.redisCacheKey.music) || null - } - const set = { - id: playListId, - data - } - - // 设置10分钟过期 - redis.set(config.constant.redisCacheKey.music, set, 60 * 10).then(() => { - debug.success('缓存更新成功,歌单ID:', playListId) - }).catch(err => { - debug.error('缓存更新失败,歌单ID:%s,错误:%s', playListId, err.message) - }) - - musicCacheLock = false - return set -} - -// 更新友链 -async function generateLinks (links = []) { - if (links && links.length) { - const githubNames = links.map(link => link.github) - const usersInfo = await getGithubUsersInfo(githubNames) - - if (usersInfo) { - return links.map((link, index) => { - const userInfo = usersInfo[index] - if (userInfo) { - link.avatar = proxy(userInfo.avatar_url) - link.slogan = userInfo.bio - link.site = link.site || userInfo.blog || userInfo.url - } - return link - }) - } - } - return links -} diff --git a/server/service/netease-music.js b/server/service/netease-music.js deleted file mode 100644 index 5209738..0000000 --- a/server/service/netease-music.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @desc 网易云音乐 TEST (备用) - * @author Jooger - * @date 30 Sep 2017 - */ - -'use strict' - -const axios = require('axios') -const NeteseMusic = require('simple-netease-cloud-music') -const { encrypt, getDebug } = require('../util') -const neteaseMusic = new NeteseMusic() -const debug = getDebug('Netease') - -const neFetcher = axios.create({ - baseURL: 'http://music.163.com', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Accept': '*/*', - 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', - 'Connection': 'keep-alive', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Referer': 'http://music.163.com', - 'Host': 'music.163.com', - 'Cookie': 'appver=2.0.2;', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36' - } -}) - -const links = { - playlist: '/weapi/v3/playlist/detail', - song: '/weapi/v3/song/detail', - songUrl: '/weapi/song/enhance/player/url' -} - -const fetchNE = function (type = '', id = '') { - return new Promise((resolve, reject) => { - if (!id) { - return reject(new Error('no id detect')) - } - let data = {} - - switch (type) { - case 'playlist': - data = { - id: id, - offset: 0, - total: true, - limit: 1000, - n: 1000, - csrf_token: '' - } - break - case 'song': - data = { - c: JSON.stringify([{ id }]), - ids: `[${id}]`, - csrf_token: '' - } - break - case 'songUrl': - data = { - ids: [id], - br: 999000, - csrf_token: '' - } - break - case 'songlyric': - data = { - id, - os: 'linux', - lv: -1, - kv: -1, - tv: -1 - } - break - default: - return reject(new Error('no support type')) - } - - neFetcher.request({ - method: 'post', - url: links[type], - params: encrypt(data) - }).then(res => { - if (res && res.status === 200) { - resolve(res.data) - } else { - reject(new Error(res.statusText)) - } - }).catch(err => { - debug.error(err.message) - }) - }) -} - -module.exports = { - neteaseMusic, - fetchNE -} diff --git a/server/util/debug.js b/server/util/debug.js deleted file mode 100644 index b967823..0000000 --- a/server/util/debug.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @desc Debug wrapper for debug - * @author Jooger - * @date 27 Sep 2017 - */ - -'use strict' - -const debug = require('debug') -const packageInfo = require('../../package.json') -const slice = Array.prototype.slice -const levelMap = { - success: { - level: 2, - emoji: '✅' - }, - info: { - level: 6, - emoji: '⚡️' - }, - warn: { - level: 3, - emoji: '⚠️' - }, - error: { - level: 1, - emoji: '❌' - } -} - -module.exports = function getDebug (namespace = '') { - const deBug = debug(`[${packageInfo.name}] ${namespace || ''}`) - function d () { - d.info.apply(d, slice.call(arguments)) - } - Object.keys(levelMap).map(key => { - d[key] = function () { - deBug.enabled = true - deBug.color = levelMap[key].level - const args = slice.call(arguments) - // args[0] = levelMap[key].emoji + ' ' + args[0] - deBug.apply(null, args) - } - }) - return d -} diff --git a/server/util/encrypt.js b/server/util/encrypt.js deleted file mode 100644 index 13688c1..0000000 --- a/server/util/encrypt.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @desc Netease service encrypt - * @author Jooger - * @date 30 Sep 2017 - */ - -'use strict' - -const crypto = require('crypto') -const bigInt = require('big-integer') -const modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' -const nonce = '0CoJUm6Qyw8W8jud' -const pubKey = '010001' - -const createSecretKey = size => { - const keys = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' - let key = '' - for (let i = 0; i < size; i++) { - let pos = Math.random() * keys.length - pos = Math.floor(pos) - key = key + keys.charAt(pos) - } - return key -} - -const aesEncrypt = (text, secKey) => { - const _text = text - const lv = Buffer.from('0102030405060708', 'binary') - const _secKey = Buffer.from(secKey, 'binary') - const cipher = crypto.createCipheriv('AES-128-CBC', _secKey, lv) - let encrypted = cipher.update(_text, 'utf8', 'base64') - encrypted += cipher.final('base64') - return encrypted -} - -const zfill = (str, size) => { - while (str.length < size) str = '0' + str - return str -} - -const rsaEncrypt = (text, pubKey, modulus) => { - const _text = text.split('').reverse().join('') - const biText = bigInt(Buffer.from(_text).toString('hex'), 16) - const biEx = bigInt(pubKey, 16) - const biMod = bigInt(modulus, 16) - const biRet = biText.modPow(biEx, biMod) - return zfill(biRet.toString(16), 256) -} - -const encrypt = params => { - const text = JSON.stringify(params) - const secKey = createSecretKey(16) - const encText = aesEncrypt(aesEncrypt(text, nonce), secKey) - const encSecKey = rsaEncrypt(secKey, pubKey, modulus) - return { - params: encText, - encSecKey: encSecKey - } -} - -module.exports = encrypt diff --git a/server/util/gravatar.js b/server/util/gravatar.js deleted file mode 100644 index 270393f..0000000 --- a/server/util/gravatar.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @desc gravatar头像 - * @author Jooger - * @date 6 Jan 2018 - */ - -'use strict' - -const gravatar = require('gravatar') -const config = require('../config') -const isProd = process.env.NODE_ENV === 'production' - -module.exports = (email = '', opt = {}) => { - if (!/^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(email)) { - return config.auth.defaultAvatar - } - - const protocol = `http${isProd ? 's' : ''}` - const url = gravatar.url(email, { - s: '100', - r: 'x', - d: 'retro', - protocol, - ...opt - }) - - return url.replace(`${protocol}://`, 'https://jooger.me/proxy/') -} diff --git a/server/util/index.js b/server/util/index.js deleted file mode 100644 index b4b8c04..0000000 --- a/server/util/index.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @desc Util entry - * @author Jooger - * @date 25 Sep 2017 - */ - -'use strict' - -const bcrypt = require('bcryptjs') -const mongoose = require('mongoose') -const validator = require('validator') -const config = require('../config') - -exports.getDebug = require('./debug') - -exports.signToken = require('./sign-token') - -exports.marked = require('./marked') - -exports.encrypt = require('./encrypt') - -exports.proxy = require('./proxy') - -exports.getLocation = require('./location') - -exports.gravatar = require('./gravatar') - -exports.noop = function () {} - -exports.isType = (obj = {}, type = 'Object') => { - if (!Array.isArray(type)) { - type = [type] - } - return type.some(t => { - if (typeof t !== 'string') { - return false - } - return Object.prototype.toString.call(obj) === `[object ${t}]` - }) -} - -exports.createObjectId = (id = '') => { - return id ? mongoose.Types.ObjectId(id) : mongoose.Types.ObjectId() -} - -exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) - -// 首字母大写 -exports.firstUpperCase = (str = '') => str.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()) - -// hash 加密 -exports.bhash = (str = '') => bcrypt.hashSync(str, 8) - -// 对比 -exports.bcompare = (str, hash) => bcrypt.compareSync(str, hash) - -// 随机字符串 -exports.randomString = (length = 8) => { - const chars = `ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz` - let id = '' - for (let i = 0; i < length; i++) { - id += chars[Math.floor(Math.random() * chars.length)] - } - return id -} - -exports.getMonthFromNum = (num = 1) => config.constant.monthMap[num - 1] || '' - -Object.keys(validator).forEach(key => { - exports[key] = function () { - return validator[key].apply(validator, arguments) - } -}) - -exports.isSiteUrl = (site = '') => validator.isURL(site, { - protocols: ['http', 'https'], - require_protocol: true -}) - -// 获取分页请求的响应数据 -exports.getDocsPaginationData = (docs = {}) => { - return { - list: docs.docs, - pagination: { - total: docs.total, - cur_page: docs.page > docs.pages ? docs.pages : docs.page, - total_page: docs.pages, - per_page: docs.limit - } - } -} diff --git a/server/util/location.js b/server/util/location.js deleted file mode 100644 index 5818a78..0000000 --- a/server/util/location.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @desc 获取ip和location - * @author Jooger - * @date 30 Oct 2017 - */ - -'use strict' - -const geoip = require('geoip-lite') - -module.exports = (req = {}) => { - const ip = (req.headers['x-forwarded-for'] || - req.headers['x-real-ip'] || - req.connection.remoteAddress || - req.socket.remoteAddress || - req.connection.socket.remoteAddress || - req.ip || - req.ips[0] || '').replace('::ffff:', '') - return { - ip, - location: geoip.lookup(ip) || {} - } -} diff --git a/server/util/marked.js b/server/util/marked.js deleted file mode 100644 index 4e1fce8..0000000 --- a/server/util/marked.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @desc Markdown renderer - * @author Jooger - * @date 26 Sep 2017 - */ - -const marked = require('marked') -const highlight = require('highlight.js') - -const languages = ['xml', 'bash', 'css', 'markdown', 'http', 'java', 'javascript', 'json', 'makefile', 'nginx', 'python', 'scss', 'sql', 'stylus'] -highlight.registerLanguage('xml', require('highlight.js/lib/languages/xml')) -highlight.registerLanguage('bash', require('highlight.js/lib/languages/bash')) -highlight.registerLanguage('css', require('highlight.js/lib/languages/css')) -highlight.registerLanguage('markdown', require('highlight.js/lib/languages/markdown')) -highlight.registerLanguage('http', require('highlight.js/lib/languages/http')) -highlight.registerLanguage('java', require('highlight.js/lib/languages/java')) -highlight.registerLanguage('javascript', require('highlight.js/lib/languages/javascript')) -highlight.registerLanguage('json', require('highlight.js/lib/languages/json')) -highlight.registerLanguage('makefile', require('highlight.js/lib/languages/makefile')) -highlight.registerLanguage('nginx', require('highlight.js/lib/languages/nginx')) -highlight.registerLanguage('python', require('highlight.js/lib/languages/python')) -highlight.registerLanguage('scss', require('highlight.js/lib/languages/scss')) -highlight.registerLanguage('sql', require('highlight.js/lib/languages/sql')) -highlight.registerLanguage('stylus', require('highlight.js/lib/languages/stylus')) -highlight.configure({ - classPrefix: '' // don't append class prefix -}) - -const renderer = new marked.Renderer() - -renderer.heading = function (text, level) { - return `${text}` -} - -renderer.link = function (href, title, text) { - const isOrigin = href.indexOf('jooger.me') > -1 - const isImage = /(/gi.test(text) - return ` - ${text} - `.replace(/\s+/g, ' ').replace('\n', '') -} - -renderer.image = function (href, title, text) { - return ` - ${text || title || href} - `.replace(/\s+/g, ' ').replace('\n', '') -} - -renderer.code = function (code, lang, escaped) { - if (this.options.highlight) { - const out = this.options.highlight(code, lang) - if (out != null && out !== code) { - escaped = true - code = out - } - } - - const lineCode = code.split('\n') - const codeWrapper = lineCode.map((line, index) => `${line}${index !== lineCode.length - 1 ? '
' : ''}`.replace(/\s+/g, ' ')).join('') - - if (!lang) { - return '
' +
-		codeWrapper +
-			'\n
' - } - - return '
' + '' +
-		codeWrapper +
-		'\n
\n' -} - -marked.setOptions({ - renderer, - gfm: true, - pedantic: false, - sanitize: false, - tables: true, - breaks: true, - smartLists: true, - smartypants: true, - highlight (code, lang) { - if (!~languages.indexOf(lang)) { - return highlight.highlightAuto(code).value - } - return highlight.highlight(lang, code).value - } -}) - -// 生成文章中的title id -function generateId (len) { - const chars = `ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz` - len = len | 8 - let id = '' - for (let i = 0; i < len; i++) { - id += chars[Math.floor(Math.random() * chars.length)] - } - return id -} - -function escape (html, encode) { - return html - .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} - -module.exports = marked diff --git a/server/util/proxy.js b/server/util/proxy.js deleted file mode 100644 index 9baf49c..0000000 --- a/server/util/proxy.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @desc Http url replace to "/proxy/..." - * @author Jooger - * @date 20 Oct 2017 - */ - -const config = require('../config') -const prefix = 'http://' - -module.exports = (url = '') => { - if (url.startsWith(prefix)) { - return url.replace(prefix, `${config.site}/proxy/`) - } - return url -} diff --git a/server/util/sign-token.js b/server/util/sign-token.js deleted file mode 100644 index a3b7f31..0000000 --- a/server/util/sign-token.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @desc jwt sign token - * @author Jooger - * @date 27 Sep 2017 - */ - -'use strict' - -const jwt = require('jsonwebtoken') -const config = require('../config') - -module.exports = (payload = {}, isLogin = true) => { - const { secrets, session } = config.auth - return jwt.sign(payload, secrets, { expiresIn: isLogin ? session.maxAge : 0 }) -} diff --git a/test/.gitkeep b/test/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/app/controller/home.test.js b/test/app/controller/home.test.js new file mode 100644 index 0000000..bcafc4a --- /dev/null +++ b/test/app/controller/home.test.js @@ -0,0 +1,21 @@ +'use strict'; + +const { app, assert } = require('egg-mock/bootstrap'); + +describe('test/app/controller/home.test.js', () => { + + it('should assert', function* () { + const pkg = require('../../../package.json'); + assert(app.config.keys.startsWith(pkg.name)); + + // const ctx = app.mockContext({}); + // yield ctx.service.xx(); + }); + + it('should GET /', () => { + return app.httpRequest() + .get('/') + .expect('hi, egg') + .expect(200); + }); +}); From efea86027cd24aef97b4e60ede35a2e34aad890c Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 22 Aug 2018 01:47:03 +0800 Subject: [PATCH 109/208] =?UTF-8?q?update:=20=E5=9F=BA=E7=A1=80=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .npmrc | 1 + app/controller/home.js | 17 +++++++-- app/extend/application.js | 12 +++++++ app/middleware/error.js | 14 ++++++++ app/middleware/gzip.js | 27 ++++++++++++++ app/middleware/headers.js | 28 +++++++++++++++ app/middleware/response.js | 39 ++++++++++++++++++++ app/model/article.js | 45 +++++++++++++++++++++++ app/model/category.js | 28 +++++++++++++++ app/model/comment.js | 47 ++++++++++++++++++++++++ app/model/setting.js | 49 +++++++++++++++++++++++++ app/model/tag.js | 26 ++++++++++++++ app/model/user.js | 35 ++++++++++++++++++ app/router.js | 13 ++++--- app/utils/index.js | 7 ++++ app/utils/share.js | 10 ++++++ app/utils/validate.js | 26 ++++++++++++++ config/config.default.js | 74 +++++++++++++++++++++++++++++++++----- config/config.local.js | 20 +++++++++++ config/config.prod.js | 10 ++++++ config/plugin.js | 14 ++++++-- package.json | 9 +++-- 23 files changed, 532 insertions(+), 20 deletions(-) create mode 100644 .npmrc create mode 100644 app/extend/application.js create mode 100644 app/middleware/error.js create mode 100644 app/middleware/gzip.js create mode 100644 app/middleware/headers.js create mode 100644 app/middleware/response.js create mode 100644 app/model/category.js create mode 100644 app/model/comment.js create mode 100644 app/model/setting.js create mode 100644 app/model/tag.js create mode 100644 app/model/user.js create mode 100644 app/utils/index.js create mode 100644 app/utils/share.js create mode 100644 app/utils/validate.js create mode 100644 config/config.local.js create mode 100644 config/config.prod.js diff --git a/.gitignore b/.gitignore index 14365a1..77b502a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ run/ .DS_Store *.sw* *.un~ +ecosystem.config.js diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..1c50a01 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry = https://registry.npm.taobao.org \ No newline at end of file diff --git a/app/controller/home.js b/app/controller/home.js index f900fd4..ebe102b 100644 --- a/app/controller/home.js +++ b/app/controller/home.js @@ -2,10 +2,21 @@ const Controller = require('egg').Controller; +const createRule = { + username: { + type: 'email', + }, + password: { + type: 'password', + compare: 're-password', + }, +} + class HomeController extends Controller { - async index() { - this.ctx.body = 'hi, egg'; - } + async index() { + this.ctx.validate(createRule) + this.ctx.body = this.ctx.request.body + } } module.exports = HomeController; diff --git a/app/extend/application.js b/app/extend/application.js new file mode 100644 index 0000000..3e28615 --- /dev/null +++ b/app/extend/application.js @@ -0,0 +1,12 @@ +const utils = require('../utils') + +const UTILS = Symbol('Application@utils') + +module.exports = { + get utils () { + if (!this[UTILS]) { + this[UTILS] = utils + } + return this[UTILS] + } +} \ No newline at end of file diff --git a/app/middleware/error.js b/app/middleware/error.js new file mode 100644 index 0000000..f0cb2d7 --- /dev/null +++ b/app/middleware/error.js @@ -0,0 +1,14 @@ +module.exports = (opt, app) => { + return async (ctx, next) => { + try { + await next() + } catch (err) { + let code = err.status || 500 + const message = app.config.codeMap[code] || err.message + if (err.code === 'invalid_param') { + code = 422 + } + ctx.fail(code, message, err.errors) + } + } +} \ No newline at end of file diff --git a/app/middleware/gzip.js b/app/middleware/gzip.js new file mode 100644 index 0000000..05aa55b --- /dev/null +++ b/app/middleware/gzip.js @@ -0,0 +1,27 @@ +/** + * @desc gzip response body + */ + +const isJSON = require('koa-is-json') +const zlib = require('zlib') + +module.exports = options => { + return async function gzip(ctx, next) { + await next() + + // 后续中间件执行完成后将响应体转换成 gzip + let body = ctx.body + if (!body) return + + // 支持 options.threshold + if (options.threshold && ctx.length < options.threshold) return + + if (isJSON(body)) body = JSON.stringify(body) + + // 设置 gzip body,修正响应头 + const stream = zlib.createGzip() + stream.end(body) + ctx.body = stream + ctx.set('Content-Encoding', 'gzip') + } +} diff --git a/app/middleware/headers.js b/app/middleware/headers.js new file mode 100644 index 0000000..a5282d1 --- /dev/null +++ b/app/middleware/headers.js @@ -0,0 +1,28 @@ +/** + * @desc 设置相应头 + */ + +module.exports = (opt, app) => { + return async (ctx, next) => { + const { request, response } = ctx + const allowedOrigins = app.config.allowedOrigins + const origin = request.get('origin') || '' + const allowed = request.query._DEV_ || + origin.includes('localhost') || + origin.includes('127.0.0.1') || + allowedOrigins.find(item => origin.includes(item)) + if (allowed) { + response.set('Access-Control-Allow-Origin', origin) + } + response.set('Access-Control-Allow-Headers', 'token, Authorization, Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With') + response.set('Access-Control-Allow-Methods', 'PUT,PATCH,POST,GET,DELETE,OPTIONS') + response.set('Access-Control-Allow-Credentials', true) + response.set('Content-Type', 'application/json;charset=utf-8') + response.set('X-Powered-By', `${app.config.name}/${app.config.version}`) + + if (request.method === 'OPTIONS') { + return ctx.success('ok') + } + await next() + } +} \ No newline at end of file diff --git a/app/middleware/response.js b/app/middleware/response.js new file mode 100644 index 0000000..db4d598 --- /dev/null +++ b/app/middleware/response.js @@ -0,0 +1,39 @@ +/** + * @desc Reponse middleware + */ + +module.exports = (opt, app) => { + const { codeMap } = app.config + const successMsg = codeMap[200] + const failMsg = codeMap[-1] + + return async (ctx, next) => { + ctx.success = (data = null, message = successMsg) => { + ctx.status = 200 + ctx.body = { + code: 200, + success: true, + message, + data + } + } + + ctx.fail = (code = -1, message = '', error = null) => { + if (app.utils.isType(code, 'String')) { + error = message || null + message = code + code = -1 + } + const body = { + code, + success: false, + message: message || codeMap[code] || failMsg + } + if (error) body.error = error + ctx.status = code === -1 ? 200 : code + ctx.body = body + } + + await next() + } +} diff --git a/app/model/article.js b/app/model/article.js index e69de29..ab0f959 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -0,0 +1,45 @@ +/** + * @desc 文章模型 + */ + +module.exports = app => { + const { mongoose } = app + const { Schema } = mongoose + + const ArticleSchema = new Schema({ + // 文章标题 + title: { type: String, required: true }, + // 文章关键字(FOR SEO) + keywords: [{ type: String }], + // 文章摘要 (FOR SEO) + description: { type: String, default: '' }, + // 文章原始markdown内容 + content: { type: String, required: true, validate: /\S+/ }, + // markdown渲染后的htmln内容 + renderedContent: { type: String, required: true, validate: /\S+/ }, + // 分类 + category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' }, + // 标签 + tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], + // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) + thumb: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, + // 文章状态 ( 0 草稿 | 1 已发布 ) + state: { type: Number, default: 0 }, + // 永久链接 + permalink: { type: String, validate: /\S+/ }, + // 创建日期 + createdAt: { type: Date, default: Date.now }, + // 更新日期 + updatedAt: { type: Date, default: Date.now }, + // 发布日期 + publishedAt: { type: Date, default: Date.now }, + // 文章元数据 (浏览量, 喜欢数, 评论数) + meta: { + pvs: { type: Number, default: 0, validate: /^\d*$/ }, + ups: { type: Number, default: 0, validate: /^\d*$/ }, + comments: { type: Number, default: 0, validate: /^\d*$/ } + } + }) + + return mongoose.model('Article', ArticleSchema) +} diff --git a/app/model/category.js b/app/model/category.js new file mode 100644 index 0000000..50a7638 --- /dev/null +++ b/app/model/category.js @@ -0,0 +1,28 @@ +/** + * @desc 分类模型 + */ + +module.exports = app => { + const { mongoose } = app + const { Schema } = mongoose + + const CategorySchema = new Schema({ + // 名称 + name: { type: String, required: true }, + // 描述 + description: { type: String, default: '' }, + // 创建日期 + createdAt: { type: Date, default: Date.now }, + // 更新日期 + updatedAt: { type: Date, default: Date.now }, + // 排序 首页分类展示顺序 + list: { type: Number, default: 1 }, + // 扩展属性 + extends: [{ + key: { type: String, validate: /\S+/ }, + value: { type: String, validate: /\S+/ } + }] + }) + + return mongoose.model('Category', CategorySchema) +} diff --git a/app/model/comment.js b/app/model/comment.js new file mode 100644 index 0000000..587cf14 --- /dev/null +++ b/app/model/comment.js @@ -0,0 +1,47 @@ +module.exports = app => { + const { mongoose } = app + const { Schema } = mongoose + + const CommentSchema = new Schema({ + // ******* 评论通用项 ************ + // 创建时间 + createdAt: { type: Date, default: Date.now }, + // 修改时间 + updatedAt: { type: Date, default: Date.now }, + // 评论内容 + content: { type: String, required: true, validate: /\S+/ }, + // marked渲染后的内容 + renderedContent: { type: String, required: true, validate: /\S+/ }, + // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 + state: { type: Number, default: 1 }, + // Akismet判定是否是垃圾评论,方便后台check + spam: { type: Boolean, default: false }, + // 评论发布者 + author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + // 点赞数 + ups: { type: Number, default: 0, validate: /^\d*$/ }, + // 是否置顶 0 否 | 1 是 + sticky: { type: Number, default: 0 }, + // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) + type: { type: Number, default: 0 }, + // type为0时此项存在 + article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, + meta: { + // 用户IP + ip: String, + // IP所在地 + location: Object, + // user agent + ua: { type: String, validate: /\S+/ }, + // refer + referer: { type: String, default: '' } + }, + // ******** 子评论具备项 ************ + // 父评论 + parent: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, // 父评论 parent和forward二者必须同时存在 + // 评论的上一级 + forward: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' } // 前一条评论ID,可以是parent的id, 比如 B评论 是 A评论的回复,则B.forward._id = A._id,主要是为了查看评论对话时的评论树构建 + }) + + return mongoose.model('Comment', CommentSchema) +} diff --git a/app/model/setting.js b/app/model/setting.js new file mode 100644 index 0000000..e9d1756 --- /dev/null +++ b/app/model/setting.js @@ -0,0 +1,49 @@ +/** + * @desc 设置参数模型 + */ + +module.exports = app => { + const { mongoose } = app + const { Schema } = mongoose + + const SettingSchema = new Schema({ + // 站点设置 + site: { + welcome: { type: String, default: '' }, + description: { type: String, default: '' }, + hobby: { type: String, default: '' }, + skill: { type: String, default: '' }, + music: { type: String, default: '' }, + location: { type: String, default: '' }, + company: { type: String, default: '' }, + links: [{ + name: { type: String, required: true }, + github: { type: String, default: '' }, + avatar: { type: String, default: '' }, + slogan: { type: String, default: '' }, + site: { type: String, required: true } + }], + musicId: { type: String, default: '' } + }, + // 第三方插件的参数 + keys: { + // 反垃圾邮件 + akismet: { + apiKey: { type: String, default: '' } + }, + // 阿里云oss + aliyun: { + accessKeyId: { type: String, default: '' }, + accessKeySecret: { type: String, default: '' }, + bucket: { type: String, default: '' }, + region: { type: String, default: '' } + }, + // 163邮箱 + mail163: { + password: { type: String, default: '' } + } + } + }) + + return mongoose.model('Setting', SettingSchema) +} diff --git a/app/model/tag.js b/app/model/tag.js new file mode 100644 index 0000000..0bbd12c --- /dev/null +++ b/app/model/tag.js @@ -0,0 +1,26 @@ +/** + * @desc 标签模型 + */ + +module.exports = app => { + const { mongoose } = app + const { Schema } = mongoose + + const TagSchema = new Schema({ + // 名称 + name: { type: String, required: true }, + // 描述 + description: { type: String, default: '' }, + // 创建日期 + createdAt: { type: Date, default: Date.now }, + // 更新日期 + updatedAt: { type: Date, default: Date.now }, + // 扩展属性 + extends: [{ + key: { type: String, validate: /\S+/ }, + value: { type: String, validate: /\S+/ } + }] + }) + + return mongoose.model('Tag', TagSchema) +} diff --git a/app/model/user.js b/app/model/user.js new file mode 100644 index 0000000..c267dbe --- /dev/null +++ b/app/model/user.js @@ -0,0 +1,35 @@ +/** + * @desc 用户模型 + */ + +module.exports = app => { + const { mongoose } = app + const { Schema } = mongoose + + const UserSchema = new Schema({ + name: { type: String, required: true }, + email: { type: String, required: true, validate: app.utils.isEmail }, + avatar: { type: String, required: true }, + site: { type: String, validate: app.utils.isSiteUrl }, + slogan: { type: String }, + description: { type: String, default: '' }, + // 角色 0 管理员 | 1 普通用户 | 2 github用户,不能更改 + role: { type: Number, default: 1 }, + // role = 0的时候才有该项 + password: { type: String }, + // 是否被禁言 + mute: { type: Boolean, default: false }, + company: { type: String, default: '' }, + location: { type: String, default: '' }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, + // github信息,不能手动更改 + github: { + id: { type: String, default: '' }, + login: { type: String, default: '' } + } + }) + + return mongoose.model('User', UserSchema) +} + diff --git a/app/router.js b/app/router.js index 7beabc2..eb8d0e7 100644 --- a/app/router.js +++ b/app/router.js @@ -1,9 +1,14 @@ -'use strict'; +'use strict' /** * @param {Egg.Application} app - egg application */ module.exports = app => { - const { router, controller } = app; - router.get('/', controller.home.index); -}; + const { router, controller } = app + router.post('/home', controller.home.index) + + router.all('*', (ctx, next) => { + const code = 404 + ctx.fail(code, app.config.codeMap[code]) + }) +} diff --git a/app/utils/index.js b/app/utils/index.js new file mode 100644 index 0000000..3770770 --- /dev/null +++ b/app/utils/index.js @@ -0,0 +1,7 @@ +const share = require('./share') +const validate = require('./validate') + +module.exports = { + ...share, + ...validate +} diff --git a/app/utils/share.js b/app/utils/share.js new file mode 100644 index 0000000..7f7ce1d --- /dev/null +++ b/app/utils/share.js @@ -0,0 +1,10 @@ +const mongoose = require('mongoose') + +exports.noop = function () {} + +// 首字母大写 +exports.firstUpperCase = (str = '') => str.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()) + +exports.createObjectId = (id = '') => { + return id ? mongoose.Types.ObjectId(id) : mongoose.Types.ObjectId() +} diff --git a/app/utils/validate.js b/app/utils/validate.js new file mode 100644 index 0000000..8baa66d --- /dev/null +++ b/app/utils/validate.js @@ -0,0 +1,26 @@ +const validator = require('validator') + +exports.isType = (obj = {}, type = 'Object') => { + if (!Array.isArray(type)) { + type = [type] + } + return type.some(t => { + if (typeof t !== 'string') { + return false + } + return Object.prototype.toString.call(obj) === `[object ${t}]` + }) +} + +exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) + +Object.keys(validator).forEach(key => { + exports[key] = function () { + return validator[key].apply(validator, arguments) + } +}) + +exports.isSiteUrl = (site = '') => validator.isURL(site, { + protocols: ['http', 'https'], + require_protocol: true +}) diff --git a/config/config.default.js b/config/config.default.js index 1b17662..f4f3ddc 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -1,15 +1,71 @@ -'use strict'; +'use strict' module.exports = appInfo => { - const config = exports = {}; + const config = exports = {} - // use for cookie sign key, should change to your own and keep security - config.keys = appInfo.name + '_1534765762288_2697'; + // use for cookie sign key, should change to your own and keep security + config.keys = appInfo.name + '_1534765762288_2697' - // add your config here - config.middleware = []; + config.version = appInfo.pkg.version - config.mongoose = {}; + config.isLocal = appInfo.env === 'local' - return config; -}; + config.isProd = process.env.EGG_SERVER_ENV === 'prod' + + // add your config here + config.middleware = [ + 'gzip', + 'response', + 'error', + 'headers' + ] + + config.bodyParser = { + jsonLimit: '10mb' + } + + config.gzip = { + threshold: 1024 + } + + config.console = { + debug: true, + error: true + } + + // mongoose配置 + config.mongoose = { + url: 'mongodb://127.0.0.1/jooger-me', + options: { + useNewUrlParser: true, + poolSize: 20, + keepAlive: true, + autoReconnect: true, + reconnectInterval: 1000, + reconnectTries: Number.MAX_VALUE + } + } + + // allowed origins + config.allowedOrigins = ['jooger.me', 'www.jooger.me', 'admin.jooger.me'] + + // 请求响应code + config.codeMap = { + '-1': '请求失败', + '200': '请求成功', + '401': '权限校验失败', + '403': 'Forbidden', + '404': '资源未找到', + '422': '参数校验失败', + '500': '服务器错误' + } + + config.onerror = { + json: (err, ctx) => { + ctx.body = { message: 'error' }; + ctx.status = 500 + } + } + + return config +} diff --git a/config/config.local.js b/config/config.local.js new file mode 100644 index 0000000..01bb24b --- /dev/null +++ b/config/config.local.js @@ -0,0 +1,20 @@ +'use strict' + +module.exports = appInfo => { + const config = exports = {} + + config.isDev = true + + config.logger = { + level: 'DEBUG', + consoleLevel: 'DEBUG', + } + + config.security = { + csrf: { + ignore: () => true + } + } + + return config +} diff --git a/config/config.prod.js b/config/config.prod.js new file mode 100644 index 0000000..28cd280 --- /dev/null +++ b/config/config.prod.js @@ -0,0 +1,10 @@ +module.exports = appInfo => { + const config = exports = {} + + config.console = { + debug: false, + error: false + } + + return config +} \ No newline at end of file diff --git a/config/plugin.js b/config/plugin.js index 8da27c1..446c8fb 100644 --- a/config/plugin.js +++ b/config/plugin.js @@ -1,9 +1,19 @@ -'use strict'; +'use strict' // had enabled by egg -// exports.static = true; +// exports.static = true exports.mongoose = { enable: true, package: 'egg-mongoose' } + +exports.validate = { + enable: true, + package: 'egg-validate', +} + +exports.console = { + enable: true, + package: 'egg-console' +} diff --git a/package.json b/package.json index b1b4cf9..2fcb7ce 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,13 @@ "private": true, "dependencies": { "egg": "^2.2.1", + "egg-console": "^2.0.1", "egg-mongoose": "^3.1.0", - "egg-scripts": "^2.5.0" + "egg-scripts": "^2.5.0", + "egg-validate": "^1.1.1", + "koa-is-json": "^1.0.0", + "validator": "^10.6.0", + "zlib": "^1.0.5" }, "devDependencies": { "autod": "^3.0.1", @@ -38,7 +43,7 @@ }, "repository": { "type": "git", - "url": "" + "url": "git@github.com:jo0ger/node-server.git" }, "author": "", "license": "MIT" From e974efd1ea224ff8678e3512d54ba052f6375cf8 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 22 Aug 2018 03:01:35 +0800 Subject: [PATCH 110/208] =?UTF-8?q?chore:=20=E4=B8=80=E4=BA=9B=E6=9D=82?= =?UTF-8?q?=E4=BA=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 6 +++ app/controller/tag.js | 25 +++++++++++++ app/extend/application.js | 11 +----- app/middleware/error.js | 10 +++-- app/middleware/response.js | 2 +- app/model/user.js | 4 +- app/router.js | 2 + app/service/proxy.js | 75 ++++++++++++++++++++++++++++++++++++++ app/service/tag.js | 8 ++++ app/utils/index.js | 7 ---- config/config.default.js | 4 +- 11 files changed, 129 insertions(+), 25 deletions(-) create mode 100644 app.js create mode 100644 app/controller/tag.js create mode 100644 app/service/proxy.js create mode 100644 app/service/tag.js delete mode 100644 app/utils/index.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..5fa99ce --- /dev/null +++ b/app.js @@ -0,0 +1,6 @@ +const path = require('path') + +module.exports = app => { + const directory = path.join(app.config.baseDir, 'app/utils') + app.loader.loadToApp(directory, 'utils') +} \ No newline at end of file diff --git a/app/controller/tag.js b/app/controller/tag.js new file mode 100644 index 0000000..8fef2cf --- /dev/null +++ b/app/controller/tag.js @@ -0,0 +1,25 @@ +/** + * @desc 标签Controller + */ + +const { Controller } = require('egg') + +module.exports = class TagController extends Controller { + async list () { + const { ctx } = this + const data = await this.service.tag.find().sort('-createdAt') + if (data) { + ctx.success(data, '标签列表获取成功') + } else { + ctx.fail('标签列表获取失败') + } + } + + async item () {} + + async create () {} + + async update () {} + + async delete () {} +} diff --git a/app/extend/application.js b/app/extend/application.js index 3e28615..d254bf9 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -1,12 +1,3 @@ -const utils = require('../utils') - -const UTILS = Symbol('Application@utils') - module.exports = { - get utils () { - if (!this[UTILS]) { - this[UTILS] = utils - } - return this[UTILS] - } + // TODO: } \ No newline at end of file diff --git a/app/middleware/error.js b/app/middleware/error.js index f0cb2d7..9187d3d 100644 --- a/app/middleware/error.js +++ b/app/middleware/error.js @@ -3,10 +3,14 @@ module.exports = (opt, app) => { try { await next() } catch (err) { + // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 + ctx.app.emit('error', err, ctx) let code = err.status || 500 - const message = app.config.codeMap[code] || err.message - if (err.code === 'invalid_param') { - code = 422 + let message = '' + if (app.config.isProd) { + message = app.config.codeMap[code] + } else { + message = err.message } ctx.fail(code, message, err.errors) } diff --git a/app/middleware/response.js b/app/middleware/response.js index db4d598..18a978f 100644 --- a/app/middleware/response.js +++ b/app/middleware/response.js @@ -19,7 +19,7 @@ module.exports = (opt, app) => { } ctx.fail = (code = -1, message = '', error = null) => { - if (app.utils.isType(code, 'String')) { + if (app.utils.validate.isType(code, 'String')) { error = message || null message = code code = -1 diff --git a/app/model/user.js b/app/model/user.js index c267dbe..11b4bad 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -8,9 +8,9 @@ module.exports = app => { const UserSchema = new Schema({ name: { type: String, required: true }, - email: { type: String, required: true, validate: app.utils.isEmail }, + email: { type: String, required: true, validate: app.utils.validate.isEmail }, avatar: { type: String, required: true }, - site: { type: String, validate: app.utils.isSiteUrl }, + site: { type: String, validate: app.utils.validate.isSiteUrl }, slogan: { type: String }, description: { type: String, default: '' }, // 角色 0 管理员 | 1 普通用户 | 2 github用户,不能更改 diff --git a/app/router.js b/app/router.js index eb8d0e7..10338c1 100644 --- a/app/router.js +++ b/app/router.js @@ -7,6 +7,8 @@ module.exports = app => { const { router, controller } = app router.post('/home', controller.home.index) + router.get('/tags', controller.tag.list) + router.all('*', (ctx, next) => { const code = 404 ctx.fail(code, app.config.codeMap[code]) diff --git a/app/service/proxy.js b/app/service/proxy.js new file mode 100644 index 0000000..c1ba612 --- /dev/null +++ b/app/service/proxy.js @@ -0,0 +1,75 @@ +/** + * @desc 公共的model proxy + */ + +const { Service } = require('egg') + +module.exports = class ProxyService extends Service { + newAndSave (docs) { + if (!Array.isArray(docs)) { + docs = [docs] + } + return this.model.insertMany(docs) + } + + paginate (query, opt = {}) { + return this.model.paginate(query, opt) + } + + findById (id) { + return this.model.findById(id) + } + + find (query = {}, opt = {}) { + return this.model.find(query, null, opt) + } + + findOne (query = {}, opt = {}) { + return this.model.findOne(query, null, opt) + } + + updateById (id, doc, opt = {}) { + return this.model.findByIdAndUpdate(id, doc, { + new: true, + ...opt + }) + } + + updateOne (query = {}, doc = {}, opt = {}) { + return this.model.findOneAndUpdate(query, doc, { + new: true, + ...opt + }) + } + + update (query = {}, doc = {}, opt = {}) { + return this.model.update(query, doc, { + multi: true, + ...opt + }) + } + + delete (query = {}) { + return this.model.remove(query) + } + + deleteById (id = '') { + return this.del({ _id: id }) + } + + deleteByIds (ids = []) { + return this.del({ + _id: { + $in: ids + } + }) + } + + aggregate (opt = {}) { + return this.model.aggregate(opt) + } + + count (query = {}) { + return this.model.count(query) + } +} \ No newline at end of file diff --git a/app/service/tag.js b/app/service/tag.js new file mode 100644 index 0000000..4f71932 --- /dev/null +++ b/app/service/tag.js @@ -0,0 +1,8 @@ +const { Service } = require('egg') +const ProxyService = require('./proxy') + +module.exports = class TagService extends ProxyService { + get model () { + return this.app.model.Tag + } +} diff --git a/app/utils/index.js b/app/utils/index.js deleted file mode 100644 index 3770770..0000000 --- a/app/utils/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const share = require('./share') -const validate = require('./validate') - -module.exports = { - ...share, - ...validate -} diff --git a/config/config.default.js b/config/config.default.js index f4f3ddc..350e448 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -10,7 +10,7 @@ module.exports = appInfo => { config.isLocal = appInfo.env === 'local' - config.isProd = process.env.EGG_SERVER_ENV === 'prod' + config.isProd = appInfo.env === 'prod' // add your config here config.middleware = [ @@ -35,7 +35,7 @@ module.exports = appInfo => { // mongoose配置 config.mongoose = { - url: 'mongodb://127.0.0.1/jooger-me', + url: 'mongodb://127.0.0.1/node-server', options: { useNewUrlParser: true, poolSize: 20, From d2656452f3bf2b92509be0fb125ac1959d8b94bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 22 Aug 2018 20:41:04 +0800 Subject: [PATCH 111/208] update: tag controller --- app.js | 6 ++ app/controller/home.js | 6 +- app/controller/tag.js | 36 +++++++++-- app/extend/application.js | 11 +++- app/extend/context.js | 10 +++ app/middleware/error.js | 1 + app/middleware/response.js | 6 +- app/model/article.js | 17 ++++-- app/model/category.js | 2 +- app/model/comment.js | 2 +- app/model/setting.js | 2 +- app/model/tag.js | 2 +- app/model/user.js | 2 +- app/router.js | 5 ++ app/service/article.js | 17 ++++++ app/service/proxy.js | 8 +-- app/service/tag.js | 122 ++++++++++++++++++++++++++++++++++++- app/utils/validate.js | 16 ++--- config/config.default.js | 14 +++-- package.json | 1 + 20 files changed, 248 insertions(+), 38 deletions(-) create mode 100644 app/extend/context.js create mode 100644 app/service/article.js diff --git a/app.js b/app.js index 5fa99ce..e7e1f78 100644 --- a/app.js +++ b/app.js @@ -3,4 +3,10 @@ const path = require('path') module.exports = app => { const directory = path.join(app.config.baseDir, 'app/utils') app.loader.loadToApp(directory, 'utils') + app.validator.addRule('objectId', (rule, val) => { + const valid = app.utils.validate.isObjectId(val) + if (!valid) { + return 'must be objectId' + } + }) } \ No newline at end of file diff --git a/app/controller/home.js b/app/controller/home.js index ebe102b..18f705e 100644 --- a/app/controller/home.js +++ b/app/controller/home.js @@ -14,8 +14,10 @@ const createRule = { class HomeController extends Controller { async index() { - this.ctx.validate(createRule) - this.ctx.body = this.ctx.request.body + // this.ctx.validate(createRule) + // this.ctx.body = this.ctx.request.body + this.ctx.throw(200, '标签已经存在' ) + this.ctx.fail(404) } } diff --git a/app/controller/tag.js b/app/controller/tag.js index 8fef2cf..79f776f 100644 --- a/app/controller/tag.js +++ b/app/controller/tag.js @@ -7,7 +7,7 @@ const { Controller } = require('egg') module.exports = class TagController extends Controller { async list () { const { ctx } = this - const data = await this.service.tag.find().sort('-createdAt') + const data = await this.service.tag.list() if (data) { ctx.success(data, '标签列表获取成功') } else { @@ -15,11 +15,37 @@ module.exports = class TagController extends Controller { } } - async item () {} + async item () { + const { ctx } = this + const data = await this.service.tag.item() + if (data) { + ctx.success(data, '标签详情获取成功') + } else { + ctx.fail('标签详情获取失败') + } + } - async create () {} + async create () { + const { ctx } = this + const data = await this.service.tag.create() + data && data.length + ? ctx.success(data[0], '标签创建成功') + : ctx.fail('标签创建失败') + } - async update () {} + async update () { + const { ctx } = this + const data = await this.service.tag.update() + data + ? ctx.success(data, '标签更新成功') + : ctx.fail('标签更新失败') + } - async delete () {} + async delete () { + const { ctx } = this + const data = await this.service.tag.delete() + data + ? ctx.success('标签删除成功') + : ctx.fail('标签删除失败') + } } diff --git a/app/extend/application.js b/app/extend/application.js index d254bf9..c95432e 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -1,3 +1,12 @@ module.exports = { - // TODO: + // model schema处理 + processSchema (schema) { + if (!schema) { + return null + } + schema.set('versionKey', false) + schema.set('toObject', { getters: true }) + schema.set('toJSON', { getters: true, virtuals: false }) + return schema + } } \ No newline at end of file diff --git a/app/extend/context.js b/app/extend/context.js new file mode 100644 index 0000000..8e3c5c5 --- /dev/null +++ b/app/extend/context.js @@ -0,0 +1,10 @@ +module.exports = { + validateObjectId (data, required) { + return this.validate({ + id: { + type: 'objectId', + required + } + }, data) + } +} \ No newline at end of file diff --git a/app/middleware/error.js b/app/middleware/error.js index 9187d3d..45c5b05 100644 --- a/app/middleware/error.js +++ b/app/middleware/error.js @@ -6,6 +6,7 @@ module.exports = (opt, app) => { // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 ctx.app.emit('error', err, ctx) let code = err.status || 500 + if (code === 200) code = -1 let message = '' if (app.config.isProd) { message = app.config.codeMap[code] diff --git a/app/middleware/response.js b/app/middleware/response.js index 18a978f..cff8bf4 100644 --- a/app/middleware/response.js +++ b/app/middleware/response.js @@ -9,6 +9,10 @@ module.exports = (opt, app) => { return async (ctx, next) => { ctx.success = (data = null, message = successMsg) => { + if (app.utils.validate.isString(data)) { + message = data + data = null + } ctx.status = 200 ctx.body = { code: 200, @@ -19,7 +23,7 @@ module.exports = (opt, app) => { } ctx.fail = (code = -1, message = '', error = null) => { - if (app.utils.validate.isType(code, 'String')) { + if (app.utils.validate.isString(code)) { error = message || null message = code code = -1 diff --git a/app/model/article.js b/app/model/article.js index ab0f959..b3a51e3 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -3,8 +3,9 @@ */ module.exports = app => { - const { mongoose } = app + const { mongoose, config } = app const { Schema } = mongoose + const articleValidateConfig = config.modelValidate.article const ArticleSchema = new Schema({ // 文章标题 @@ -18,13 +19,19 @@ module.exports = app => { // markdown渲染后的htmln内容 renderedContent: { type: String, required: true, validate: /\S+/ }, // 分类 - category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' }, + category: { type: Schema.Types.ObjectId, ref: 'Category' }, // 标签 - tag: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }], + tag: [{ type: Schema.Types.ObjectId, ref: 'Tag' }], // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) thumb: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, // 文章状态 ( 0 草稿 | 1 已发布 ) - state: { type: Number, default: 0 }, + state: { + type: Number, + default: articleValidateConfig.state.default, + validate: (val) => { + return Object.values(articleValidateConfig.state.optional).includes(val) + } + }, // 永久链接 permalink: { type: String, validate: /\S+/ }, // 创建日期 @@ -41,5 +48,5 @@ module.exports = app => { } }) - return mongoose.model('Article', ArticleSchema) + return mongoose.model('Article', app.processSchema(ArticleSchema)) } diff --git a/app/model/category.js b/app/model/category.js index 50a7638..6dda587 100644 --- a/app/model/category.js +++ b/app/model/category.js @@ -24,5 +24,5 @@ module.exports = app => { }] }) - return mongoose.model('Category', CategorySchema) + return mongoose.model('Category', app.processSchema(CategorySchema)) } diff --git a/app/model/comment.js b/app/model/comment.js index 587cf14..b5ff8a0 100644 --- a/app/model/comment.js +++ b/app/model/comment.js @@ -43,5 +43,5 @@ module.exports = app => { forward: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' } // 前一条评论ID,可以是parent的id, 比如 B评论 是 A评论的回复,则B.forward._id = A._id,主要是为了查看评论对话时的评论树构建 }) - return mongoose.model('Comment', CommentSchema) + return mongoose.model('Comment', app.processSchema(CommentSchema)) } diff --git a/app/model/setting.js b/app/model/setting.js index e9d1756..8611718 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -45,5 +45,5 @@ module.exports = app => { } }) - return mongoose.model('Setting', SettingSchema) + return mongoose.model('Setting', app.processSchema(SettingSchema)) } diff --git a/app/model/tag.js b/app/model/tag.js index 0bbd12c..519d5d6 100644 --- a/app/model/tag.js +++ b/app/model/tag.js @@ -22,5 +22,5 @@ module.exports = app => { }] }) - return mongoose.model('Tag', TagSchema) + return mongoose.model('Tag', app.processSchema(TagSchema)) } diff --git a/app/model/user.js b/app/model/user.js index 11b4bad..c6f01b1 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -30,6 +30,6 @@ module.exports = app => { } }) - return mongoose.model('User', UserSchema) + return mongoose.model('User', app.processSchema(UserSchema)) } diff --git a/app/router.js b/app/router.js index 10338c1..8b52881 100644 --- a/app/router.js +++ b/app/router.js @@ -8,6 +8,11 @@ module.exports = app => { router.post('/home', controller.home.index) router.get('/tags', controller.tag.list) + router.get('/tags/:id', controller.tag.item) + router.post('/tags', controller.tag.create) + router.put('/tags/:id', controller.tag.update) + router.patch('/tags/:id', controller.tag.update) + router.delete('/tags/:id', controller.tag.delete) router.all('*', (ctx, next) => { const code = 404 diff --git a/app/service/article.js b/app/service/article.js new file mode 100644 index 0000000..2364b99 --- /dev/null +++ b/app/service/article.js @@ -0,0 +1,17 @@ +/** + * @desc Article Services + */ + +const ProxyService = require('./proxy') + +module.exports = class ArticleService extends ProxyService { + get model () { + return this.app.model.Article + } + + get rules () { + return { + // todo + } + } +} diff --git a/app/service/proxy.js b/app/service/proxy.js index c1ba612..dc5ec95 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -1,5 +1,5 @@ /** - * @desc 公共的model proxy + * @desc 公共的model proxy service */ const { Service } = require('egg') @@ -49,16 +49,16 @@ module.exports = class ProxyService extends Service { }) } - delete (query = {}) { + remove (query = {}) { return this.model.remove(query) } deleteById (id = '') { - return this.del({ _id: id }) + return this.model.deleteOne({ _id: id }) } deleteByIds (ids = []) { - return this.del({ + return this.model.deleteMany({ _id: { $in: ids } diff --git a/app/service/tag.js b/app/service/tag.js index 4f71932..2593150 100644 --- a/app/service/tag.js +++ b/app/service/tag.js @@ -1,8 +1,128 @@ -const { Service } = require('egg') +/** + * @desc Tag Services + */ + const ProxyService = require('./proxy') module.exports = class TagService extends ProxyService { get model () { return this.app.model.Tag } + + get rules () { + return { + list: { + // 查询关键词 + keyword: { type: 'string', required: false } + }, + create: { + name: { type: 'string', required: true }, + keyword: { type: 'string', required: false }, + extends: { + type: 'array', + required: false, + itemType: 'object', + rule: { + key: 'string', + value: 'string' + } + } + }, + update: { + name: { type: 'string', required: false }, + keyword: { type: 'string', required: false }, + extends: { + type: 'array', + required: false, + itemType: 'object', + rule: { + key: 'string', + value: 'string' + } + } + } + } + } + + async list () { + const { ctx, app, service } = this + ctx.validate(this.rules.list, ctx.query) + const query = {} + const { keyword } = ctx.query + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { name: keywordReg } + ] + } + const data = await this.find(query).sort('-createdAt').exec() + + if (data) { + const isFunction = app.utils.validate.isFunction + const PUBLISH = app.config.modelValidate.article.state.optional.PUBLISH + await Promise.all(data.map((item, index) => { + const toObject = item.toObject + if (isFunction(toObject)) { + item = item.toObject() + } + return service.article.find({ + tag: item._id, + state: PUBLISH + }).exec().then(articles => { + item.count = articles.length + data[index] = item + }) + })) + } + + return data + } + + async item () { + const { ctx } = this + const { params } = ctx + ctx.validateObjectId(params) + let data = await this.findById(params.id).exec() + if (data) { + data = data.toObject() + const articles = await this.service.article.find({ tag: params.id }) + .select('-tag') + .exec() + data.articles = articles + data.articlesCount = articles.length + } + return data + } + + async create () { + const { ctx } = this + const { body } = ctx.request + ctx.validate(this.rules.create, body) + const exists = await this.find({ name: body.name }).exec() + if (exists && exists.length) { + ctx.throw(200, '标签已经存在') + } + return await this.newAndSave(body) + } + + async update () { + const { ctx } = this + const { params } = ctx + const { body } = ctx.request + ctx.validateObjectId(params) + ctx.validate(this.rules.update, body) + return await this.updateById(params.id, body).exec() + } + + async delete () { + const { ctx } = this + const { params } = ctx + ctx.validateObjectId(params) + const articles = await this.service.article.find({ tag: params.id }).exec() + if (articles && articles.length) { + ctx.throw(200, '该标签下有文章,不能删除') + } + const data = await this.deleteById(params.id).exec() + return data && data.ok && data.n + } } diff --git a/app/utils/validate.js b/app/utils/validate.js index 8baa66d..9b3eec7 100644 --- a/app/utils/validate.js +++ b/app/utils/validate.js @@ -1,16 +1,12 @@ +const lodash = require('lodash') +const mongoose = require('mongoose') const validator = require('validator') -exports.isType = (obj = {}, type = 'Object') => { - if (!Array.isArray(type)) { - type = [type] +Object.keys(lodash).forEach(key => { + if (key.startsWith('is')) { + exports[key] = lodash[key] } - return type.some(t => { - if (typeof t !== 'string') { - return false - } - return Object.prototype.toString.call(obj) === `[object ${t}]` - }) -} +}) exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) diff --git a/config/config.default.js b/config/config.default.js index 350e448..c97bc9c 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -60,10 +60,16 @@ module.exports = appInfo => { '500': '服务器错误' } - config.onerror = { - json: (err, ctx) => { - ctx.body = { message: 'error' }; - ctx.status = 500 + config.modelValidate = { + article: { + // 文章状态 ( 0 草稿(默认) | 1 已发布 ) + state: { + default: 0, + optional: { + 'DRAFT': 0, + 'PUBLISH': 1 + } + } } } diff --git a/package.json b/package.json index 2fcb7ce..01ba08f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "egg-scripts": "^2.5.0", "egg-validate": "^1.1.1", "koa-is-json": "^1.0.0", + "lodash": "^4.17.10", "validator": "^10.6.0", "zlib": "^1.0.5" }, From 1ffe1b8ec08492a0323af79f0dde513188182a69 Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 23 Aug 2018 00:23:35 +0800 Subject: [PATCH 112/208] update: category controller done --- app/controller/category.js | 51 +++++++++++++++ app/controller/home.js | 24 ------- app/middleware/auth.js | 0 app/middleware/error.js | 4 ++ app/router.js | 63 ++++++++++++++---- app/service/category.js | 130 +++++++++++++++++++++++++++++++++++++ 6 files changed, 235 insertions(+), 37 deletions(-) create mode 100644 app/controller/category.js delete mode 100644 app/controller/home.js create mode 100644 app/middleware/auth.js create mode 100644 app/service/category.js diff --git a/app/controller/category.js b/app/controller/category.js new file mode 100644 index 0000000..a43bce0 --- /dev/null +++ b/app/controller/category.js @@ -0,0 +1,51 @@ +/** + * @desc 分类 Controller + */ + +const { Controller } = require('egg') + +module.exports = class CategoryController extends Controller { + async list () { + const { ctx } = this + const data = await this.service.category.list() + if (data) { + ctx.success(data, '分类列表获取成功') + } else { + ctx.fail('分类列表获取失败') + } + } + + async item () { + const { ctx } = this + const data = await this.service.category.item() + if (data) { + ctx.success(data, '分类详情获取成功') + } else { + ctx.fail('分类详情获取失败') + } + } + + async create () { + const { ctx } = this + const data = await this.service.category.create() + data && data.length + ? ctx.success(data[0], '分类创建成功') + : ctx.fail('分类创建失败') + } + + async update () { + const { ctx } = this + const data = await this.service.category.update() + data + ? ctx.success(data, '分类更新成功') + : ctx.fail('分类更新失败') + } + + async delete () { + const { ctx } = this + const data = await this.service.category.delete() + data + ? ctx.success('分类删除成功') + : ctx.fail('分类删除失败') + } +} diff --git a/app/controller/home.js b/app/controller/home.js deleted file mode 100644 index 18f705e..0000000 --- a/app/controller/home.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -const Controller = require('egg').Controller; - -const createRule = { - username: { - type: 'email', - }, - password: { - type: 'password', - compare: 're-password', - }, -} - -class HomeController extends Controller { - async index() { - // this.ctx.validate(createRule) - // this.ctx.body = this.ctx.request.body - this.ctx.throw(200, '标签已经存在' ) - this.ctx.fail(404) - } -} - -module.exports = HomeController; diff --git a/app/middleware/auth.js b/app/middleware/auth.js new file mode 100644 index 0000000..e69de29 diff --git a/app/middleware/error.js b/app/middleware/error.js index 45c5b05..915764f 100644 --- a/app/middleware/error.js +++ b/app/middleware/error.js @@ -1,3 +1,7 @@ +/** + * @desc 统一错误处理 + */ + module.exports = (opt, app) => { return async (ctx, next) => { try { diff --git a/app/router.js b/app/router.js index 8b52881..6a24f38 100644 --- a/app/router.js +++ b/app/router.js @@ -1,21 +1,58 @@ -'use strict' - -/** - * @param {Egg.Application} app - egg application - */ module.exports = app => { - const { router, controller } = app - router.post('/home', controller.home.index) + const { router, config } = app - router.get('/tags', controller.tag.list) - router.get('/tags/:id', controller.tag.item) - router.post('/tags', controller.tag.create) - router.put('/tags/:id', controller.tag.update) - router.patch('/tags/:id', controller.tag.update) - router.delete('/tags/:id', controller.tag.delete) + router.get('/', async (ctx, next) => { + ctx.body = { + name: config.name, + version: config.version, + author: config.pkg.author, + github: 'https://github.com/jo0ger', + site: config.site, + poweredBy: ['Egg', 'Koa2', 'MongoDB', 'Nginx', 'Redis'] + } + }) + + frontend(app) + backend(app) router.all('*', (ctx, next) => { const code = 404 ctx.fail(code, app.config.codeMap[code]) }) } + +function frontend (app) { + const { router, controller } = app + + // Category + router.get('/categories', controller.category.list) + router.get('/categories/:id', controller.category.item) + + // Tag + router.get('/tags', controller.tag.list) + router.get('/tags/:id', controller.tag.item) + + return router +} + +function backend (app) { + const { router, controller } = app + + // Category + router.get('/backend/categories', controller.category.list) + router.get('/backend/categories/:id', controller.category.item) + router.post('/backend/categories', controller.category.create) + router.put('/backend/categories/:id', controller.category.update) + router.patch('/backend/categories/:id', controller.category.update) + router.delete('/backend/categories/:id', controller.category.delete) + + // Tag + router.get('/backend/tags', controller.tag.list) + router.get('/backend/tags/:id', controller.tag.item) + router.post('/backend/tags', controller.tag.create) + router.put('/backend/tags/:id', controller.tag.update) + router.patch('/backend/tags/:id', controller.tag.update) + router.delete('/backend/tags/:id', controller.tag.delete) + + return router +} diff --git a/app/service/category.js b/app/service/category.js new file mode 100644 index 0000000..68998cd --- /dev/null +++ b/app/service/category.js @@ -0,0 +1,130 @@ +/** + * @desc 分类 Services + */ + +const ProxyService = require('./proxy') + +module.exports = class CategoryService extends ProxyService { + get model () { + return this.app.model.Category + } + + get rules () { + return { + list: { + // 查询关键词 + keyword: { type: 'string', required: false } + }, + create: { + name: { type: 'string', required: true }, + description: { type: 'string', required: false }, + list: { type: 'number', required: true }, + extends: { + type: 'array', + required: false, + itemType: 'object', + rule: { + key: 'string', + value: 'string' + } + } + }, + update: { + name: { type: 'string', required: false }, + description: { type: 'string', required: false }, + list: { type: 'number', required: true }, + extends: { + type: 'array', + required: false, + itemType: 'object', + rule: { + key: 'string', + value: 'string' + } + } + } + } + } + + async list () { + const { ctx, app, service } = this + ctx.validate(this.rules.list, ctx.query) + const query = {} + const { keyword } = ctx.query + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { name: keywordReg } + ] + } + const data = await this.find(query).sort('-createdAt').exec() + + if (data) { + const isFunction = app.utils.validate.isFunction + const PUBLISH = app.config.modelValidate.article.state.optional.PUBLISH + await Promise.all(data.map((item, index) => { + const toObject = item.toObject + if (isFunction(toObject)) { + item = item.toObject() + } + return service.article.find({ + category: item._id, + state: PUBLISH + }).exec().then(articles => { + item.count = articles.length + data[index] = item + }) + })) + } + + return data + } + + async item () { + const { ctx } = this + const { params } = ctx + ctx.validateObjectId(params) + let data = await this.findById(params.id).exec() + if (data) { + data = data.toObject() + const articles = await this.service.article.find({ category: params.id }) + .select('-category') + .exec() + data.articles = articles + data.articlesCount = articles.length + } + return data + } + + async create () { + const { ctx } = this + const { body } = ctx.request + ctx.validate(this.rules.create, body) + const exists = await this.find({ name: body.name }).exec() + if (exists && exists.length) { + ctx.throw(200, '分类已经存在') + } + return await this.newAndSave(body) + } + + async update () { + const { ctx } = this + const { params } = ctx + const { body } = ctx.request + ctx.validateObjectId(params) + ctx.validate(this.rules.update, body) + return await this.updateById(params.id, body).exec() + } + + async delete () { + const { ctx } = this + const { params } = ctx + ctx.validateObjectId(params) + const articles = await this.service.article.find({ category: params.id }).exec() + if (articles && articles.length) { + ctx.throw(200, '该分类下有文章,不能删除') + } + const data = await this.deleteById(params.id).exec() + return data && data.ok && data.n + } +} From 93d72b84f7a9457d9bb375ab9c7b84bd171923a8 Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 23 Aug 2018 00:50:58 +0800 Subject: [PATCH 113/208] update: auth middleware done --- app/middleware/auth.js | 51 ++++++++++++++++++++++++++++++++++++++++ app/router.js | 26 ++++++++++---------- app/service/user.js | 17 ++++++++++++++ config/config.default.js | 10 ++++++++ package.json | 2 ++ 5 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 app/service/user.js diff --git a/app/middleware/auth.js b/app/middleware/auth.js index e69de29..2f7aeee 100644 --- a/app/middleware/auth.js +++ b/app/middleware/auth.js @@ -0,0 +1,51 @@ +/** + * @desc jwt 校验 + */ + +const compose = require('koa-compose') +const jwt = require('jsonwebtoken') + +module.exports = (app) => { + const { config, service } = app + return compose([ + verifyToken(app), + async (ctx, next) => { + if (!ctx.session._verify) { + return ctx.fail(401) + } + const userId = ctx.cookies.get(config.userCookieKey, { signed: false }) + const user = await service.user.findById(userId).exec() + if (!user) { + return ctx.fail(401, '用户不存在') + } + ctx._user = user.toObject() + ctx._isAuthenticated = true + await next() + } + ]) +} + +// 验证本地登录token +function verifyToken (app) { + const { config, logger } = app + return async (ctx, next) => { + ctx.session._verify = false + const token = ctx.cookies.get(config.session.key) + if (token) { + let decodedToken = null + try { + decodedToken = await jwt.verify(token, config.secrets) + } catch (err) { + logger.error('token校验出错,错误:' + err.message) + return ctx.throw(401, err) + } + if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { + // 已校验权限 + ctx.session._verify = true + ctx.session._token = token + logger.success('token校验成功') + } + } + await next() + } +} diff --git a/app/router.js b/app/router.js index 6a24f38..e9f524e 100644 --- a/app/router.js +++ b/app/router.js @@ -36,23 +36,23 @@ function frontend (app) { } function backend (app) { - const { router, controller } = app + const { router, controller, middlewares } = app // Category - router.get('/backend/categories', controller.category.list) - router.get('/backend/categories/:id', controller.category.item) - router.post('/backend/categories', controller.category.create) - router.put('/backend/categories/:id', controller.category.update) - router.patch('/backend/categories/:id', controller.category.update) - router.delete('/backend/categories/:id', controller.category.delete) + router.get('/backend/categories', middlewares.auth(app), controller.category.list) + router.get('/backend/categories/:id', middlewares.auth(app), controller.category.item) + router.post('/backend/categories', middlewares.auth(app), controller.category.create) + router.put('/backend/categories/:id', middlewares.auth(app), controller.category.update) + router.patch('/backend/categories/:id', middlewares.auth(app), controller.category.update) + router.delete('/backend/categories/:id', middlewares.auth(app), controller.category.delete) // Tag - router.get('/backend/tags', controller.tag.list) - router.get('/backend/tags/:id', controller.tag.item) - router.post('/backend/tags', controller.tag.create) - router.put('/backend/tags/:id', controller.tag.update) - router.patch('/backend/tags/:id', controller.tag.update) - router.delete('/backend/tags/:id', controller.tag.delete) + router.get('/backend/tags', middlewares.auth(app), controller.tag.list) + router.get('/backend/tags/:id', middlewares.auth(app), controller.tag.item) + router.post('/backend/tags', middlewares.auth(app), controller.tag.create) + router.put('/backend/tags/:id', middlewares.auth(app), controller.tag.update) + router.patch('/backend/tags/:id', middlewares.auth(app), controller.tag.update) + router.delete('/backend/tags/:id', middlewares.auth(app), controller.tag.delete) return router } diff --git a/app/service/user.js b/app/service/user.js new file mode 100644 index 0000000..d97f166 --- /dev/null +++ b/app/service/user.js @@ -0,0 +1,17 @@ +/** + * @desc User Services + */ + +const ProxyService = require('./proxy') + +module.exports = class UserService extends ProxyService { + get model () { + return this.app.model.User + } + + get rules () { + return { + // todo + } + } +} diff --git a/config/config.default.js b/config/config.default.js index c97bc9c..e847c89 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -20,6 +20,16 @@ module.exports = appInfo => { 'headers' ] + config.session = { + key: appInfo.name + '-token', + maxAge: 60000 * 60 * 24 * 7, + signed: false + } + + config.userCookieKey = appInfo.name + '_userid' + + config.secrets = appInfo.name + '_secrets' + config.bodyParser = { jsonLimit: '10mb' } diff --git a/package.json b/package.json index 01ba08f..6e8a712 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "egg-mongoose": "^3.1.0", "egg-scripts": "^2.5.0", "egg-validate": "^1.1.1", + "jsonwebtoken": "^8.3.0", + "koa-compose": "^4.1.0", "koa-is-json": "^1.0.0", "lodash": "^4.17.10", "validator": "^10.6.0", From e86974d1bafdf8ca01fccaa1053212bb2d89743e Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 23 Aug 2018 01:34:27 +0800 Subject: [PATCH 114/208] update: redis --- app.js | 25 +++++++++++++++++++++++-- app/controller/article.js | 23 +++++++++++++++++++++++ app/controller/category.js | 16 ++++++---------- app/controller/tag.js | 17 +++++++---------- app/controller/user.js | 23 +++++++++++++++++++++++ app/model/user.js | 11 +++++++++-- app/router.js | 32 ++++++++++++++++++++------------ app/service/article.js | 20 ++++++++++++++++++++ app/service/proxy.js | 2 +- app/service/user.js | 22 ++++++++++++++++++++++ app/utils/token.js | 10 ++++++++++ config/config.default.js | 32 ++++++++++++++++++++++++++++++-- config/plugin.js | 5 +++++ package.json | 1 + 14 files changed, 200 insertions(+), 39 deletions(-) create mode 100644 app/controller/article.js create mode 100644 app/controller/user.js create mode 100644 app/utils/token.js diff --git a/app.js b/app.js index e7e1f78..8f6f260 100644 --- a/app.js +++ b/app.js @@ -1,12 +1,33 @@ const path = require('path') module.exports = app => { - const directory = path.join(app.config.baseDir, 'app/utils') - app.loader.loadToApp(directory, 'utils') + app.loader.loadToApp(path.join(app.config.baseDir, 'app/utils'), 'utils') app.validator.addRule('objectId', (rule, val) => { const valid = app.utils.validate.isObjectId(val) if (!valid) { return 'must be objectId' } }) + app.sessionStore = class Store { + constructor(app) { + this.app = app + this.client = app.redis.get('token') + } + + async get (key) { + const res = await this.client.get(key) + if (!res) return null + return JSON.parse(res) + } + + async set (key, value, maxAge) { + if (!maxAge) maxAge = 24 * 60 * 60 * 1000; + value = JSON.stringify(value); + await this.client.set(key, value, 'PX', maxAge); + } + + async destroy (key) { + await this.client.del(key) + } + } } \ No newline at end of file diff --git a/app/controller/article.js b/app/controller/article.js new file mode 100644 index 0000000..1f5d7fc --- /dev/null +++ b/app/controller/article.js @@ -0,0 +1,23 @@ +/** + * @desc 文章 Controller + */ + +const { Controller } = require('egg') + +module.exports = class ArticleController extends Controller { + async list () { + const { ctx } = this + const data = await this.service.article.list() + data + ? ctx.success(data, '文章列表获取成功') + : ctx.fail('文章列表获取失败') + } + + async item () { + const { ctx } = this + const data = await this.service.article.item() + data + ? ctx.success(data, '文章详情获取成功') + : ctx.fail('文章详情获取失败') + } +} diff --git a/app/controller/category.js b/app/controller/category.js index a43bce0..c14dd16 100644 --- a/app/controller/category.js +++ b/app/controller/category.js @@ -8,21 +8,17 @@ module.exports = class CategoryController extends Controller { async list () { const { ctx } = this const data = await this.service.category.list() - if (data) { - ctx.success(data, '分类列表获取成功') - } else { - ctx.fail('分类列表获取失败') - } + data + ? ctx.success(data, '分类列表获取成功') + : ctx.fail('分类列表获取失败') } async item () { const { ctx } = this const data = await this.service.category.item() - if (data) { - ctx.success(data, '分类详情获取成功') - } else { - ctx.fail('分类详情获取失败') - } + data + ? ctx.success(data, '分类详情获取成功') + : ctx.fail('分类详情获取失败') } async create () { diff --git a/app/controller/tag.js b/app/controller/tag.js index 79f776f..8c2b11f 100644 --- a/app/controller/tag.js +++ b/app/controller/tag.js @@ -8,21 +8,18 @@ module.exports = class TagController extends Controller { async list () { const { ctx } = this const data = await this.service.tag.list() - if (data) { - ctx.success(data, '标签列表获取成功') - } else { - ctx.fail('标签列表获取失败') - } + this.app.utils.share.noop() + data + ? ctx.success(data, '标签列表获取成功') + : ctx.fail('标签列表获取失败') } async item () { const { ctx } = this const data = await this.service.tag.item() - if (data) { - ctx.success(data, '标签详情获取成功') - } else { - ctx.fail('标签详情获取失败') - } + data + ? ctx.success(data, '标签详情获取成功') + : ctx.fail('标签详情获取失败') } async create () { diff --git a/app/controller/user.js b/app/controller/user.js new file mode 100644 index 0000000..5ab07e6 --- /dev/null +++ b/app/controller/user.js @@ -0,0 +1,23 @@ +/** + * @desc 用户Controller + */ + +const { Controller } = require('egg') + +module.exports = class UserController extends Controller { + async list () { + const { ctx } = this + const data = await this.service.user.list() + data + ? ctx.success(data, '用户列表获取成功') + : ctx.fail('用户列表获取失败') + } + + async item () { + const { ctx } = this + const data = await this.service.user.item() + data + ? ctx.success(data, '用户详情获取成功') + : ctx.fail('用户详情获取失败') + } +} diff --git a/app/model/user.js b/app/model/user.js index c6f01b1..acdab4e 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -3,8 +3,9 @@ */ module.exports = app => { - const { mongoose } = app + const { mongoose, config } = app const { Schema } = mongoose + const userValidateConfig = config.modelValidate.user const UserSchema = new Schema({ name: { type: String, required: true }, @@ -14,7 +15,13 @@ module.exports = app => { slogan: { type: String }, description: { type: String, default: '' }, // 角色 0 管理员 | 1 普通用户 | 2 github用户,不能更改 - role: { type: Number, default: 1 }, + role: { + type: Number, + default: userValidateConfig.role.default, + validate: (val) => { + return Object.values(userValidateConfig.role.optional).includes(val) + } + }, // role = 0的时候才有该项 password: { type: String }, // 是否被禁言 diff --git a/app/router.js b/app/router.js index e9f524e..11c3f67 100644 --- a/app/router.js +++ b/app/router.js @@ -32,27 +32,35 @@ function frontend (app) { router.get('/tags', controller.tag.list) router.get('/tags/:id', controller.tag.item) + // User + router.get('/users/:id', controller.user.item) + return router } function backend (app) { const { router, controller, middlewares } = app + const auth = middlewares.auth(app) // Category - router.get('/backend/categories', middlewares.auth(app), controller.category.list) - router.get('/backend/categories/:id', middlewares.auth(app), controller.category.item) - router.post('/backend/categories', middlewares.auth(app), controller.category.create) - router.put('/backend/categories/:id', middlewares.auth(app), controller.category.update) - router.patch('/backend/categories/:id', middlewares.auth(app), controller.category.update) - router.delete('/backend/categories/:id', middlewares.auth(app), controller.category.delete) + router.get('/backend/categories', auth, controller.category.list) + router.get('/backend/categories/:id', auth, controller.category.item) + router.post('/backend/categories', auth, controller.category.create) + router.put('/backend/categories/:id', auth, controller.category.update) + router.patch('/backend/categories/:id', auth, controller.category.update) + router.delete('/backend/categories/:id', auth, controller.category.delete) // Tag - router.get('/backend/tags', middlewares.auth(app), controller.tag.list) - router.get('/backend/tags/:id', middlewares.auth(app), controller.tag.item) - router.post('/backend/tags', middlewares.auth(app), controller.tag.create) - router.put('/backend/tags/:id', middlewares.auth(app), controller.tag.update) - router.patch('/backend/tags/:id', middlewares.auth(app), controller.tag.update) - router.delete('/backend/tags/:id', middlewares.auth(app), controller.tag.delete) + router.get('/backend/tags', auth, controller.tag.list) + router.get('/backend/tags/:id', auth, controller.tag.item) + router.post('/backend/tags', auth, controller.tag.create) + router.put('/backend/tags/:id', auth, controller.tag.update) + router.patch('/backend/tags/:id', auth, controller.tag.update) + router.delete('/backend/tags/:id', auth, controller.tag.delete) + + // User + router.get('/backend/users', auth, controller.user.list) + router.get('/backend/users/:id', auth, controller.user.item) return router } diff --git a/app/service/article.js b/app/service/article.js index 2364b99..1bce4a1 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -14,4 +14,24 @@ module.exports = class ArticleService extends ProxyService { // todo } } + + async list () {} + + async item () {} + + async create () {} + + async update () {} + + async delete () { + const { ctx } = this + const { params } = ctx + ctx.validateObjectId(params) + const data = await this.deleteById(params.id).exec() + return data && data.ok && data.n + } + + async like () {} + + async archives () {} } diff --git a/app/service/proxy.js b/app/service/proxy.js index dc5ec95..0bfb0c9 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -42,7 +42,7 @@ module.exports = class ProxyService extends Service { }) } - update (query = {}, doc = {}, opt = {}) { + updateMany (query = {}, doc = {}, opt = {}) { return this.model.update(query, doc, { multi: true, ...opt diff --git a/app/service/user.js b/app/service/user.js index d97f166..9aa087d 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -14,4 +14,26 @@ module.exports = class UserService extends ProxyService { // todo } } + + async list () { + const { ctx } = this + let select = '-password' + if (!ctx._isAuthenticated) { + select += ' -createdAt -updatedAt -role' + } + return await this.find().sort('-createdAt').select(select).exec() + } + + async item () { + const { ctx } = this + const { params } = ctx + ctx.validateObjectId(params) + let select = '-password' + if (!ctx._isAuthenticated) { + select += ' -createdAt -updatedAt -github' + } + return await this.findById(params.id).select(select).exec() + } + + async update () {} } diff --git a/app/utils/token.js b/app/utils/token.js new file mode 100644 index 0000000..64d6028 --- /dev/null +++ b/app/utils/token.js @@ -0,0 +1,10 @@ +/** + * @desc jwt sign token + */ + +const jwt = require('jsonwebtoken') + +exports.sign = (app, payload = {}, isLogin = true) => { + const { secrets, session } = app.config.auth + return jwt.sign(payload, secrets, { expiresIn: isLogin ? session.maxAge : 0 }) +} \ No newline at end of file diff --git a/config/config.default.js b/config/config.default.js index e847c89..2ff2394 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -56,6 +56,23 @@ module.exports = appInfo => { } } + config.redis = { + clients: { + token: { + host: '127.0.0.1', + port: 6379, + db: 0, + password: appInfo.name + }, + util: { + host: '127.0.0.1', + port: 6379, + db: 1, + password: appInfo.name + } + } + } + // allowed origins config.allowedOrigins = ['jooger.me', 'www.jooger.me', 'admin.jooger.me'] @@ -76,8 +93,19 @@ module.exports = appInfo => { state: { default: 0, optional: { - 'DRAFT': 0, - 'PUBLISH': 1 + DRAFT: 0, + PUBLISH: 1 + } + } + }, + user: { + // 角色 0 管理员 | 1 普通用户 | 2 gayhub用户,不能更改 + role: { + default: 1, + optional: { + ADMIN: 0, + NORMAL: 1, + GAYHUB: 2 } } } diff --git a/config/plugin.js b/config/plugin.js index 446c8fb..f244c73 100644 --- a/config/plugin.js +++ b/config/plugin.js @@ -17,3 +17,8 @@ exports.console = { enable: true, package: 'egg-console' } + +exports.redis = { + enable: true, + package: 'egg-redis' +} diff --git a/package.json b/package.json index 6e8a712..084c16d 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "egg": "^2.2.1", "egg-console": "^2.0.1", "egg-mongoose": "^3.1.0", + "egg-redis": "^2.0.0", "egg-scripts": "^2.5.0", "egg-validate": "^1.1.1", "jsonwebtoken": "^8.3.0", From bd33f6b092076d1afd07ec27fe92d8d166e84de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Thu, 23 Aug 2018 17:50:24 +0800 Subject: [PATCH 115/208] update: some article services --- .eslintrc | 7 +- app.js | 24 +++- app/controller/article.js | 12 +- app/controller/auth.js | 29 +++++ app/controller/category.js | 14 +- app/controller/comment.js | 19 +++ app/controller/setting.js | 23 ++++ app/controller/tag.js | 14 +- app/controller/user.js | 6 +- app/extend/application.js | 9 +- app/extend/context.js | 2 +- app/middleware/auth.js | 76 +++++------ app/middleware/error.js | 2 +- app/middleware/gzip.js | 2 +- app/middleware/headers.js | 2 +- app/middleware/response.js | 1 - app/model/article.js | 4 +- app/model/setting.js | 5 + app/model/user.js | 2 +- app/router.js | 38 ++++-- app/service/article.js | 212 ++++++++++++++++++++++++++++++- app/service/auth.js | 129 +++++++++++++++++++ app/service/proxy.js | 103 +++++++-------- app/service/setting.js | 83 ++++++++++++ app/service/user.js | 25 +++- app/service/util.js | 90 +++++++++++++ app/utils/encode.js | 17 +++ app/utils/share.js | 16 ++- app/utils/token.js | 5 +- app/utils/validate.js | 16 +-- config/config.default.js | 40 +++--- config/config.local.js | 10 +- config/config.prod.js | 4 +- package.json | 9 +- test/app/controller/home.test.js | 24 ++-- 35 files changed, 887 insertions(+), 187 deletions(-) create mode 100644 app/controller/auth.js create mode 100644 app/controller/comment.js create mode 100644 app/controller/setting.js create mode 100644 app/service/auth.js create mode 100644 app/service/setting.js create mode 100644 app/service/util.js create mode 100644 app/utils/encode.js diff --git a/.eslintrc b/.eslintrc index 60cc69f..ea915a3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,11 @@ { "extends": "eslint-config-egg", "rules": { - "indent": 4 + "indent": ["error", 4], + "semi": 0, + "space-before-function-paren": [ "error", "always"], + "strict": 0, + "comma-dangle": 0, + "array-bracket-spacing": 0 } } \ No newline at end of file diff --git a/app.js b/app.js index 8f6f260..4769e20 100644 --- a/app.js +++ b/app.js @@ -1,15 +1,35 @@ +'use strict' + const path = require('path') module.exports = app => { app.loader.loadToApp(path.join(app.config.baseDir, 'app/utils'), 'utils') + addValidateRule(app) + setSessionstore(app) + app.beforeStart(async () => { + const ctx = app.createAnonymousContext() + await ctx.service.auth.seed() + }) +} + +function addValidateRule (app) { app.validator.addRule('objectId', (rule, val) => { const valid = app.utils.validate.isObjectId(val) if (!valid) { return 'must be objectId' } }) + app.validator.addRule('email', (rule, val) => { + return app.utils.validate.isEmail(val) + }) + app.validator.addRule('url', (rule, val) => { + return app.utils.validate.isSiteUrl(val) + }) +} + +function setSessionstore (app) { app.sessionStore = class Store { - constructor(app) { + constructor (app) { this.app = app this.client = app.redis.get('token') } @@ -30,4 +50,4 @@ module.exports = app => { await this.client.del(key) } } -} \ No newline at end of file +} diff --git a/app/controller/article.js b/app/controller/article.js index 1f5d7fc..35f168b 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -3,7 +3,7 @@ */ const { Controller } = require('egg') - + module.exports = class ArticleController extends Controller { async list () { const { ctx } = this @@ -20,4 +20,14 @@ module.exports = class ArticleController extends Controller { ? ctx.success(data, '文章详情获取成功') : ctx.fail('文章详情获取失败') } + + async create () {} + + async update () {} + + async delete () {} + + async like () {} + + async archives () {} } diff --git a/app/controller/auth.js b/app/controller/auth.js new file mode 100644 index 0000000..1732b69 --- /dev/null +++ b/app/controller/auth.js @@ -0,0 +1,29 @@ +/** + * @desc Auth Controller + */ + +const { + Controller +} = require('egg') + +module.exports = class AuthController extends Controller { + async login () { + await this.service.auth.login() + } + + async logout () { + await this.service.auth.logout() + } + + async info () { + await this.service.auth.info() + } + + async update () { + const { ctx } = this + const data = await this.service.auth.update() + data + ? ctx.success(data, '信息更新成功') + : ctx.fail('信息更新失败') + } +} diff --git a/app/controller/category.js b/app/controller/category.js index c14dd16..9264b64 100644 --- a/app/controller/category.js +++ b/app/controller/category.js @@ -3,7 +3,7 @@ */ const { Controller } = require('egg') - + module.exports = class CategoryController extends Controller { async list () { const { ctx } = this @@ -25,23 +25,23 @@ module.exports = class CategoryController extends Controller { const { ctx } = this const data = await this.service.category.create() data && data.length - ? ctx.success(data[0], '分类创建成功') - : ctx.fail('分类创建失败') + ? ctx.success(data[0], '分类创建成功') + : ctx.fail('分类创建失败') } async update () { const { ctx } = this const data = await this.service.category.update() data - ? ctx.success(data, '分类更新成功') - : ctx.fail('分类更新失败') + ? ctx.success(data, '分类更新成功') + : ctx.fail('分类更新失败') } async delete () { const { ctx } = this const data = await this.service.category.delete() data - ? ctx.success('分类删除成功') - : ctx.fail('分类删除失败') + ? ctx.success('分类删除成功') + : ctx.fail('分类删除失败') } } diff --git a/app/controller/comment.js b/app/controller/comment.js new file mode 100644 index 0000000..be85dda --- /dev/null +++ b/app/controller/comment.js @@ -0,0 +1,19 @@ +/** + * @desc 评论 Controller + */ + +const { Controller } = require('egg') + +module.exports = class CommentController extends Controller { + async list () {} + + async item () {} + + async create () {} + + async update () {} + + async delete () {} + + async like () {} +} diff --git a/app/controller/setting.js b/app/controller/setting.js new file mode 100644 index 0000000..50cdcf9 --- /dev/null +++ b/app/controller/setting.js @@ -0,0 +1,23 @@ +/** + * @desc Setting Controller + */ + +const { Controller } = require('egg') + +module.exports = class SettingController extends Controller { + async index () { + const { ctx } = this + const data = await this.service.setting.index() + data + ? ctx.success(data, '数据获取成功') + : ctx.fail('数据获取失败') + } + + async update () { + const { ctx } = this + const data = await this.service.setting.update() + data + ? ctx.success(data, '数据更新成功') + : ctx.fail('数据更新失败') + } +} diff --git a/app/controller/tag.js b/app/controller/tag.js index 8c2b11f..a663541 100644 --- a/app/controller/tag.js +++ b/app/controller/tag.js @@ -3,7 +3,7 @@ */ const { Controller } = require('egg') - + module.exports = class TagController extends Controller { async list () { const { ctx } = this @@ -26,23 +26,23 @@ module.exports = class TagController extends Controller { const { ctx } = this const data = await this.service.tag.create() data && data.length - ? ctx.success(data[0], '标签创建成功') - : ctx.fail('标签创建失败') + ? ctx.success(data[0], '标签创建成功') + : ctx.fail('标签创建失败') } async update () { const { ctx } = this const data = await this.service.tag.update() data - ? ctx.success(data, '标签更新成功') - : ctx.fail('标签更新失败') + ? ctx.success(data, '标签更新成功') + : ctx.fail('标签更新失败') } async delete () { const { ctx } = this const data = await this.service.tag.delete() data - ? ctx.success('标签删除成功') - : ctx.fail('标签删除失败') + ? ctx.success('标签删除成功') + : ctx.fail('标签删除失败') } } diff --git a/app/controller/user.js b/app/controller/user.js index 5ab07e6..fa9a594 100644 --- a/app/controller/user.js +++ b/app/controller/user.js @@ -3,7 +3,7 @@ */ const { Controller } = require('egg') - + module.exports = class UserController extends Controller { async list () { const { ctx } = this @@ -20,4 +20,8 @@ module.exports = class UserController extends Controller { ? ctx.success(data, '用户详情获取成功') : ctx.fail('用户详情获取失败') } + + async update () {} + + async password () {} } diff --git a/app/extend/application.js b/app/extend/application.js index c95432e..b77b0e7 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -1,12 +1,17 @@ +const mongoosePaginate = require('mongoose-paginate-v2') + module.exports = { // model schema处理 - processSchema (schema) { + processSchema (schema, paginate) { if (!schema) { return null } schema.set('versionKey', false) schema.set('toObject', { getters: true }) schema.set('toJSON', { getters: true, virtuals: false }) + if (paginate) { + schema.plugin(mongoosePaginate) + } return schema } -} \ No newline at end of file +} diff --git a/app/extend/context.js b/app/extend/context.js index 8e3c5c5..9bc7c76 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -7,4 +7,4 @@ module.exports = { } }, data) } -} \ No newline at end of file +} diff --git a/app/middleware/auth.js b/app/middleware/auth.js index 2f7aeee..2a2558d 100644 --- a/app/middleware/auth.js +++ b/app/middleware/auth.js @@ -5,47 +5,51 @@ const compose = require('koa-compose') const jwt = require('jsonwebtoken') -module.exports = (app) => { - const { config, service } = app +module.exports = app => { return compose([ verifyToken(app), - async (ctx, next) => { - if (!ctx.session._verify) { - return ctx.fail(401) - } - const userId = ctx.cookies.get(config.userCookieKey, { signed: false }) - const user = await service.user.findById(userId).exec() - if (!user) { - return ctx.fail(401, '用户不存在') - } - ctx._user = user.toObject() - ctx._isAuthenticated = true - await next() - } + async (ctx, next) => { + if (!ctx.session._verify) { + return ctx.fail(401) + } + const userId = ctx.cookies.get(app.config.userCookieKey, { + signed: false + }) + const user = await ctx.service.user.findById(userId).exec() + if (!user) { + return ctx.fail(401, '用户不存在') + } + ctx._user = user.toObject() + ctx._isAuthenticated = true + await next() + } ]) } -// 验证本地登录token +// 验证登录token function verifyToken (app) { - const { config, logger } = app - return async (ctx, next) => { - ctx.session._verify = false - const token = ctx.cookies.get(config.session.key) - if (token) { - let decodedToken = null - try { - decodedToken = await jwt.verify(token, config.secrets) - } catch (err) { - logger.error('token校验出错,错误:' + err.message) + const { + config, + logger + } = app + return async (ctx, next) => { + ctx.session._verify = false + const token = ctx.cookies.get(config.session.key) + if (token) { + let decodedToken = null + try { + decodedToken = await jwt.verify(token, config.secrets) + } catch (err) { + logger.error('Token校验出错,错误:' + err.message) return ctx.throw(401, err) - } - if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { - // 已校验权限 - ctx.session._verify = true - ctx.session._token = token - logger.success('token校验成功') - } - } - await next() - } + } + if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { + // 已校验权限 + ctx.session._verify = true + ctx.session._token = token + logger.info('Token校验成功') + } + } + await next() + } } diff --git a/app/middleware/error.js b/app/middleware/error.js index 915764f..ac9b292 100644 --- a/app/middleware/error.js +++ b/app/middleware/error.js @@ -20,4 +20,4 @@ module.exports = (opt, app) => { ctx.fail(code, message, err.errors) } } -} \ No newline at end of file +} diff --git a/app/middleware/gzip.js b/app/middleware/gzip.js index 05aa55b..649e601 100644 --- a/app/middleware/gzip.js +++ b/app/middleware/gzip.js @@ -6,7 +6,7 @@ const isJSON = require('koa-is-json') const zlib = require('zlib') module.exports = options => { - return async function gzip(ctx, next) { + return async function gzip (ctx, next) { await next() // 后续中间件执行完成后将响应体转换成 gzip diff --git a/app/middleware/headers.js b/app/middleware/headers.js index a5282d1..bafc56c 100644 --- a/app/middleware/headers.js +++ b/app/middleware/headers.js @@ -25,4 +25,4 @@ module.exports = (opt, app) => { } await next() } -} \ No newline at end of file +} diff --git a/app/middleware/response.js b/app/middleware/response.js index cff8bf4..63e0e5c 100644 --- a/app/middleware/response.js +++ b/app/middleware/response.js @@ -21,7 +21,6 @@ module.exports = (opt, app) => { data } } - ctx.fail = (code = -1, message = '', error = null) => { if (app.utils.validate.isString(code)) { error = message || null diff --git a/app/model/article.js b/app/model/article.js index b3a51e3..4829827 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -28,7 +28,7 @@ module.exports = app => { state: { type: Number, default: articleValidateConfig.state.default, - validate: (val) => { + validate: val => { return Object.values(articleValidateConfig.state.optional).includes(val) } }, @@ -48,5 +48,5 @@ module.exports = app => { } }) - return mongoose.model('Article', app.processSchema(ArticleSchema)) + return mongoose.model('Article', app.processSchema(ArticleSchema, true)) } diff --git a/app/model/setting.js b/app/model/setting.js index 8611718..dcdbf2b 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -41,6 +41,11 @@ module.exports = app => { // 163邮箱 mail163: { password: { type: String, default: '' } + }, + // gayhub + github: { + clientID: { type: String, default: '' }, + clientSecret: { type: String, default: '' } } } }) diff --git a/app/model/user.js b/app/model/user.js index acdab4e..00590a9 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -18,7 +18,7 @@ module.exports = app => { role: { type: Number, default: userValidateConfig.role.default, - validate: (val) => { + validate: val => { return Object.values(userValidateConfig.role.optional).includes(val) } }, diff --git a/app/router.js b/app/router.js index 11c3f67..92763a9 100644 --- a/app/router.js +++ b/app/router.js @@ -1,21 +1,21 @@ module.exports = app => { const { router, config } = app - router.get('/', async (ctx, next) => { - ctx.body = { - name: config.name, - version: config.version, - author: config.pkg.author, - github: 'https://github.com/jo0ger', - site: config.site, - poweredBy: ['Egg', 'Koa2', 'MongoDB', 'Nginx', 'Redis'] - } - }) + router.get('/', async ctx => { + ctx.body = { + name: config.name, + version: config.version, + author: config.pkg.author, + github: 'https://github.com/jo0ger', + site: config.site, + poweredBy: ['Egg', 'Koa2', 'MongoDB', 'Nginx', 'Redis'] + } + }) frontend(app) backend(app) - router.all('*', (ctx, next) => { + router.all('*', ctx => { const code = 404 ctx.fail(code, app.config.codeMap[code]) }) @@ -24,6 +24,9 @@ module.exports = app => { function frontend (app) { const { router, controller } = app + // Article + router.get('/articles', controller.article.list) + // Category router.get('/categories', controller.category.list) router.get('/categories/:id', controller.category.item) @@ -35,6 +38,9 @@ function frontend (app) { // User router.get('/users/:id', controller.user.item) + // Setting + router.get('/setting', controller.setting.index) + return router } @@ -62,5 +68,15 @@ function backend (app) { router.get('/backend/users', auth, controller.user.list) router.get('/backend/users/:id', auth, controller.user.item) + // Setting + router.get('/backend/setting', auth, controller.setting.index) + router.put('/backend/setting', auth, controller.setting.update) + router.patch('/backend/setting', auth, controller.setting.update) + + // Auth + router.post('/backend/auth/login', controller.auth.login) + router.get('/backend/auth/logout', auth, controller.auth.logout) + router.get('/backend/auth/info', auth, controller.auth.info) + return router } diff --git a/app/service/article.js b/app/service/article.js index 1bce4a1..5f8176c 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -3,7 +3,7 @@ */ const ProxyService = require('./proxy') - + module.exports = class ArticleService extends ProxyService { get model () { return this.app.model.Article @@ -11,13 +11,155 @@ module.exports = class ArticleService extends ProxyService { get rules () { return { - // todo + list: { + page: { type: 'number', required: true, min: 1 }, + limit: { type: 'number', required: true, min: 1 }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + category: { type: 'objectId', required: false }, + tag: { type: 'objectId', required: false }, + keyword: { type: 'string', required: false }, + startDate: { type: 'dateTime', required: false }, + endDate: { type: 'dateTime', required: false }, + // -1 desc | 1 asc + order: { type: 'enum', values: [-1, 1], required: false }, + sortBy: { type: 'enum', values: ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], required: false } + }, + item: { + // 后台用,只获取当前文章内容,不获取相关文章和上下篇文章 + single: { type: 'boolean', required: false } + } } } - async list () {} + async list () { + const { ctx } = this + ctx.query.page = Number(ctx.query.page) + ctx.query.limit = Number(ctx.query.limit) + ctx.validate(this.rules.list, ctx.query) + const { page, limit, state, keyword, category, tag, order, sortBy, startDate, endDate } = ctx.query + const options = { + sort: { + updatedAt: -1, + createdAt: -1 + }, + page, + limit, + lean: true, + select: '-content -renderedContent', + populate: [ + { + path: 'category', + select: 'name description extends' + }, { + path: 'tag', + select: 'name description' + } + ] + } + const query = {} + if (state !== undefined) { + query.state = state + } + + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { title: keywordReg } + ] + } + + // 分类 + if (category) { + // 如果是id + if (this.app.utils.validate.isObjectId(category)) { + query.category = category + } else { + // 普通字符串,需要先查到id + const c = await this.service.category.findOne({ name: category }).exec() + query.category = c ? c._id : this.app.utils.share.createObjectId() + } + } - async item () {} + // 标签 + if (tag) { + // 如果是id + if (this.app.utils.validate.isObjectId(tag)) { + query.tag = tag + } else { + // 普通字符串,需要先查到id + const t = await this.service.tag.findOne({ name: tag }).exec() + query.tag = t ? t._id : this.app.utils.share.createObjectId() + } + } + + // 未通过权限校验(前台获取文章列表) + if (!ctx._isAuthenticated) { + // 将文章状态重置为1 + query.state = 1 + // 文章列表不需要content和state + options.select = '-content -renderedContent -state' + } else { + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } + + // 起始日期 + if (startDate) { + const $gte = new Date(startDate) + if ($gte.toString() !== 'Invalid Date') { + query.createdAt = { $gte } + } + } + + // 结束日期 + if (endDate) { + const $lte = new Date(endDate) + if ($lte.toString() !== 'Invalid Date') { + query.createdAt = Object.assign({}, query.createdAt, { $lte }) + } + } + } + + const data = await this.service.article.paginate(query, options) + return this.app.utils.share.getDocsPaginationData(data) + } + + async item () { + const { ctx } = this + const { params } = ctx + ctx.validateObjectId(params) + ctx.validate(this.rules.item, ctx.query) + let query = null + // 只有前台博客访问文章的时候pv才+1 + if (!ctx._isAuthenticated) { + query = this.updateOne({ _id: params.id, state: this.config.modelValidate.article.optional.PUBLISH }, { $inc: { 'meta.pvs': 1 } }).select('-content') + } else { + query = this.findById(params.id) + } + let data = await query.populate([ + { + path: 'category', + select: 'name description extends' + }, { + path: 'tag', + select: 'name description extends' + } + ]).exec() + if (!ctx.query.single) { + // 获取相关文章和上下篇文章 + data = data.toObject() + const [related, adjacent] = await Promise.all([ + this.getRelatedArticles(data), + this.getAdjacentArticles(data) + ]) + data.related = related + data.adjacent = adjacent + } + return data + } async create () {} @@ -34,4 +176,66 @@ module.exports = class ArticleService extends ProxyService { async like () {} async archives () {} + + // 根据标签获取相关文章 + async getRelatedArticles (data) { + if (!data || !data._id) return null + const { _id, tag = [] } = data + const articles = await this.service.article.find({ + _id: { $nin: [ _id ] }, + state: 1, + tag: { $in: tag.map(t => t._id) } + }) + .select('title thumb createdAt publishedAt meta category') + .populate({ + path: 'category', + select: 'name description' + }) + .exec() + .catch(err => { + this.logger.error('相关文章查询失败,错误:' + err.message) + return null + }) + return articles && articles.slice(0, 10) || null + } + + // 获取相邻的文章 + async getAdjacentArticles (ctx, data) { + if (!data || !data._id) return null + const query = {} + // 如果未通过权限校验,将文章状态重置为1 + if (!ctx._isAuthenticated) { + query.state = this.config.modelValidate.article.optional.PUBLISH + } + const prev = await this.service.article.findOne(query) + .select('title createdAt publishedAt thumb category') + .populate({ + path: 'category', + select: 'name description' + }) + .sort('-createdAt') + .lt('createdAt', data.createdAt) + .exec() + .catch(err => { + this.logger.error('前一篇文章获取失败,错误:' + err.message) + return null + }) + const next = await this.service.article.findOne(query) + .select('title createdAt publishedAt thumb category') + .populate({ + path: 'category', + select: 'name description' + }) + .sort('createdAt') + .gt('createdAt', data.createdAt) + .exec() + .catch(err => { + this.logger.error('后一篇文章获取失败,错误:' + err.message) + return null + }) + return { + prev: prev ? prev.toObject() : null, + next: next ? next.toObject() : null + } + } } diff --git a/app/service/auth.js b/app/service/auth.js new file mode 100644 index 0000000..ea7203d --- /dev/null +++ b/app/service/auth.js @@ -0,0 +1,129 @@ +/** + * @desc Auth Services + */ + +const { Service } = require('egg') + +module.exports = class AuthService extends Service { + get rules () { + return { + login: { + username: { type: 'string', required: true }, + password: { type: 'string', required: true } + }, + update: { + name: { type: 'string', required: false }, + email: { type: 'email', required: false }, + site: { type: 'url', required: false }, + description: { type: 'string', required: false }, + avatar: { type: 'string', required: false }, + slogan: { type: 'string', required: false }, + company: { type: 'string', required: false }, + location: { type: 'string', required: false } + } + } + } + + async login () { + const { ctx } = this + const { body } = ctx.request + ctx.validate(this.rules.login, body) + const user = await this.service.user.findOne({ name: body.username }).exec() + if (!user) { + return ctx.fail('用户不存在') + } + const vertifyPassword = this.app.utils.encode.bcompare(body.password, user.password) + if (vertifyPassword) { + const { key, domain, maxAge, signed } = this.app.config.session + const token = this.app.utils.token.sign(this.app, { + id: user._id, + name: user.name + }) + ctx.cookies.set(key, token, { signed, domain, maxAge, httpOnly: false }) + ctx.cookies.set(this.app.config.userCookieKey, user._id, { signed, domain, maxAge, httpOnly: false }) + this.logger.info(`用户登录成功, ID:${user._id},用户名:${user.name}`) + ctx.success({ + id: user._id, + token + }, '登录成功') + } else { + ctx.fail('密码错误') + } + } + + async logout () { + const { ctx } = this + const { key, domain, signed } = this.app.config.session + const token = this.app.utils.token.sign(this.app, { + id: ctx._user._id, + name: ctx._user.name + }, false) + ctx.cookies.set(key, token, { signed, domain, maxAge: 0, httpOnly: false }) + ctx.cookies.set(this.app.config.auth.userCookieKey, ctx._user._id, { signed, domain, maxAge: 0, httpOnly: false }) + this.logger.info(`用户登出成功, 用户ID:${ctx.user._id},用户名:${ctx.user.name}`) + ctx.success('登出成功') + } + + async info () { + const { ctx } = this + const adminId = ctx._user._id + if (!adminId && !ctx._isAuthenticated) { + return ctx.fail(401) + } + let data = null + if (ctx._isAuthenticated) { + data = await this.service.user.findById(adminId).select('-password').exec() + } + if (data) { + ctx.success({ + info: data, + token: ctx.session._token + }) + } else { + ctx.fail(401) + } + } + + async update () { + const { ctx } = this + const { body } = ctx.request + ctx.validate(this.rules.update, body) + return await this.service.user.updateById(ctx._user_id, body) + } + + async seed () { + const ADMIN = this.config.modelValidate.user.role.optional.ADMIN + const exist = await this.service.user.findOne({ role: ADMIN }).exec() + if (!exist) { + await this.create() + } + } + + async create (name) { + const ADMIN = this.config.modelValidate.user.role.optional.ADMIN + const defaultAdmin = this.config.defaultAdmin + const admin = await this.service.util.getGithubUserInfo(name || defaultAdmin.name) + if (!admin) { + return this.logger.warn('管理员创建失败') + } + const data = await this.service.user.newAndSave({ + role: ADMIN, + name: admin.name, + email: admin.email || this.config.pkg.author.email, + password: this.app.utils.encode.bhash(defaultAdmin.password), + slogan: admin.bio, + site: admin.blog || admin.url, + avatar: this.service.util.proxyUrl(admin.avatar_url), + company: admin.company, + location: admin.location, + github: { + id: admin.id, + login: admin.login + } + }) + if (!data || !data.length) { + return this.logger.warn(`管理员【${admin.name}】创建失败`) + } + this.logger.info(`管理员【${admin.name}】创建成功`) + } +} diff --git a/app/service/proxy.js b/app/service/proxy.js index 0bfb0c9..2cb88c4 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -5,71 +5,62 @@ const { Service } = require('egg') module.exports = class ProxyService extends Service { - newAndSave (docs) { - if (!Array.isArray(docs)) { - docs = [docs] - } - return this.model.insertMany(docs) - } + newAndSave (docs) { + if (!Array.isArray(docs)) { + docs = [docs] + } + return this.model.insertMany(docs) + } - paginate (query, opt = {}) { - return this.model.paginate(query, opt) - } + paginate (query, opt = {}) { + return this.model.paginate(query, opt) + } - findById (id) { - return this.model.findById(id) - } + findById (id) { + return this.model.findById(id) + } - find (query = {}, opt = {}) { - return this.model.find(query, null, opt) - } + find (query = {}, opt = {}) { + return this.model.find(query, null, opt) + } - findOne (query = {}, opt = {}) { - return this.model.findOne(query, null, opt) - } + findOne (query = {}, opt = {}) { + return this.model.findOne(query, null, opt) + } - updateById (id, doc, opt = {}) { - return this.model.findByIdAndUpdate(id, doc, { - new: true, - ...opt - }) - } + updateById (id, doc, opt = {}) { + return this.model.findByIdAndUpdate(id, doc, Object.assign({ new: true }, opt)) + } - updateOne (query = {}, doc = {}, opt = {}) { - return this.model.findOneAndUpdate(query, doc, { - new: true, - ...opt - }) - } + updateOne (query = {}, doc = {}, opt = {}) { + return this.model.findOneAndUpdate(query, doc, Object.assign({ new: true }, opt)) + } - updateMany (query = {}, doc = {}, opt = {}) { - return this.model.update(query, doc, { - multi: true, - ...opt - }) - } + updateMany (query = {}, doc = {}, opt = {}) { + return this.model.update(query, doc, Object.assign({ multi: true }, opt)) + } - remove (query = {}) { - return this.model.remove(query) - } + remove (query = {}) { + return this.model.remove(query) + } - deleteById (id = '') { - return this.model.deleteOne({ _id: id }) - } + deleteById (id = '') { + return this.model.deleteOne({ _id: id }) + } - deleteByIds (ids = []) { - return this.model.deleteMany({ - _id: { - $in: ids - } - }) - } + deleteByIds (ids = []) { + return this.model.deleteMany({ + _id: { + $in: ids + } + }) + } - aggregate (opt = {}) { - return this.model.aggregate(opt) - } + aggregate (opt = {}) { + return this.model.aggregate(opt) + } - count (query = {}) { - return this.model.count(query) - } -} \ No newline at end of file + count (query = {}) { + return this.model.count(query) + } +} diff --git a/app/service/setting.js b/app/service/setting.js new file mode 100644 index 0000000..382e930 --- /dev/null +++ b/app/service/setting.js @@ -0,0 +1,83 @@ +/** + * @desc Setting Services + */ + +const ProxyService = require('./proxy') + +module.exports = class SettingService extends ProxyService { + get model () { + return this.app.model.Setting + } + + get rules () { + return { + index: { + filter: { type: 'string', required: false } + }, + create: { + site: { + type: 'object', + required: true + }, + keys: { + type: 'object', + required: true + } + }, + update: { + site: { + type: 'object', + required: false + }, + keys: { + type: 'object', + required: false + } + } + } + } + + async index () { + const { ctx } = this + ctx.validate(this.rules.index, ctx.query) + const query = {} + const { filter } = ctx.query + if (filter) { + query.select = filter.split(',').join(' ') + } + if (!ctx._isAuthenticated) { + query.select = 'site' + } + return await this.findOne(query).exec() + } + + async keys () { + return await this.findOne().select('keys').exec() + } + + async create (payload) { + const { ctx } = this + const body = payload || ctx.request.body + ctx.validate(this.rules.create, body) + const exist = await this.findOne().exec() + if (exist) { + ctx.throw(200, '分类已经存在') + } + return await this.newAndSave(body) + } + + async update (payload) { + if (!payload) { + // http request + payload = await this.findOne().exec() + if (!payload) return + } + // 更新友链 + payload.site.links = await this.service.util.generateLinks(payload.site.links) + const data = await this.updateOne({}, payload).exec() + if (data) { + this.logger.info('Setting更新成功') + } + return data + } +} diff --git a/app/service/user.js b/app/service/user.js index 9aa087d..c1d9ca1 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -3,7 +3,7 @@ */ const ProxyService = require('./proxy') - + module.exports = class UserService extends ProxyService { get model () { return this.app.model.User @@ -11,7 +11,10 @@ module.exports = class UserService extends ProxyService { get rules () { return { - // todo + password: { + password: { type: 'string', required: true }, + oldPassword: { type: 'string', required: true } + } } } @@ -21,7 +24,10 @@ module.exports = class UserService extends ProxyService { if (!ctx._isAuthenticated) { select += ' -createdAt -updatedAt -role' } - return await this.find().sort('-createdAt').select(select).exec() + return await this.find() + .sort('-createdAt') + .select(select) + .exec() } async item () { @@ -35,5 +41,16 @@ module.exports = class UserService extends ProxyService { return await this.findById(params.id).select(select).exec() } - async update () {} + async password () { + const { ctx } = this + const { body } = ctx.request + ctx.validate(this.rules.password, body) + const verify = this.app.utils.encode.bcompare(body.oldPassword, ctx._user.password) + if (!verify) { + ctx.throw(200, '原密码错误') + } + return await this.updateById(ctx._user._id, { + password: this.app.utils.encode.bhash(body.password) + }).exec() + } } diff --git a/app/service/util.js b/app/service/util.js new file mode 100644 index 0000000..e869167 --- /dev/null +++ b/app/service/util.js @@ -0,0 +1,90 @@ +/** + * @desc Util Services + */ + +const { Service } = require('egg') +const axios = require('axios') + +const prefix = 'http://' + +module.exports = class UtilService extends Service { + proxyUrl (url) { + if (url.startsWith(prefix)) { + return url.replace(prefix, `${this.app.config.site}/proxy/`) + } + return url + } + + async getGithubUserInfo (username) { + if (!username) return null + let gayhub = {} + if (this.config.isLocal) { + gayhub = this.config.github + } else { + const keys = this.service.setting.keys() + if (!keys || !keys.github) { + this.logger.warn('未找到gayhub配置') + return null + } + gayhub = keys.github + } + const { clientID, clientSecret } = gayhub + try { + const res = await axios.get(`https://api.github.com/users/${username}`, { + params: { + client_id: clientID, + client_secret: clientSecret + } + }, { + headers: { + Accept: 'application/json' + } + }) + if (res && res.status === 200) { + this.logger.info(`【 ${username} 】信息抓取成功`) + return res.data + } + return null + } catch (error) { + this.logger.warn(`【 ${username} 】信息抓取失败`) + this.logger.error(error) + return null + } + } + + async getGithubUsersInfo (usernames = '') { + if (!usernames) { + return null + } else if (this.app.utils.validate.isString(usernames)) { + usernames = [usernames] + } else if (!Array.isArray(usernames)) { + return null + } + return await Promise.all(usernames.map(name => this.getGithubUserInfo(name))) + } + + async getGithubAuthUserInfo (access_token) { + return await axios.get('https://api.github.com/user', { + params: { access_token } + }) + } + + async generateLinks (links = []) { + if (links && links.length) { + const githubNames = links.map(link => link.github) + const usersInfo = await this.getGithubUsersInfo(githubNames) + if (usersInfo) { + return links.map((link, index) => { + const userInfo = usersInfo[index] + if (userInfo) { + link.avatar = this.proxyUrl(userInfo.avatar_url) + link.slogan = userInfo.bio + link.site = link.site || userInfo.blog || userInfo.url + } + return link + }) + } + } + return links + } +} diff --git a/app/utils/encode.js b/app/utils/encode.js new file mode 100644 index 0000000..1773c05 --- /dev/null +++ b/app/utils/encode.js @@ -0,0 +1,17 @@ +const bcrypt = require('bcryptjs') + +// hash 加密 +exports.bhash = (str = '') => bcrypt.hashSync(str, 8) + +// 对比 +exports.bcompare = bcrypt.compareSync + +// 随机字符串 +exports.randomString = (length = 8) => { + const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz' + let id = '' + for (let i = 0; i < length; i++) { + id += chars[Math.floor(Math.random() * chars.length)] + } + return id +} diff --git a/app/utils/share.js b/app/utils/share.js index 7f7ce1d..110c91a 100644 --- a/app/utils/share.js +++ b/app/utils/share.js @@ -6,5 +6,19 @@ exports.noop = function () {} exports.firstUpperCase = (str = '') => str.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()) exports.createObjectId = (id = '') => { - return id ? mongoose.Types.ObjectId(id) : mongoose.Types.ObjectId() + return id ? mongoose.Types.ObjectId(id) : mongoose.Types.ObjectId() +} + +// 获取分页请求的响应数据 +exports.getDocsPaginationData = docs => { + if (!docs) return null + return { + list: docs.docs, + pageInfo: { + total: docs.totalDocs, + current: docs.page > docs.totalPages ? docs.totalPages : docs.page, + pages: docs.totalPages, + limit: docs.limit + } + } } diff --git a/app/utils/token.js b/app/utils/token.js index 64d6028..0f740be 100644 --- a/app/utils/token.js +++ b/app/utils/token.js @@ -5,6 +5,5 @@ const jwt = require('jsonwebtoken') exports.sign = (app, payload = {}, isLogin = true) => { - const { secrets, session } = app.config.auth - return jwt.sign(payload, secrets, { expiresIn: isLogin ? session.maxAge : 0 }) -} \ No newline at end of file + return jwt.sign(payload, app.config.secrets, { expiresIn: isLogin ? app.config.session.maxAge : 0 }) +} diff --git a/app/utils/validate.js b/app/utils/validate.js index 9b3eec7..ecd2091 100644 --- a/app/utils/validate.js +++ b/app/utils/validate.js @@ -3,20 +3,20 @@ const mongoose = require('mongoose') const validator = require('validator') Object.keys(lodash).forEach(key => { - if (key.startsWith('is')) { - exports[key] = lodash[key] - } + if (key.startsWith('is')) { + exports[key] = lodash[key] + } }) exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str) Object.keys(validator).forEach(key => { - exports[key] = function () { - return validator[key].apply(validator, arguments) - } + exports[key] = function () { + return validator[key].apply(validator, arguments) + } }) exports.isSiteUrl = (site = '') => validator.isURL(site, { - protocols: ['http', 'https'], - require_protocol: true + protocols: ['http', 'https'], + require_protocol: true }) diff --git a/config/config.default.js b/config/config.default.js index 2ff2394..91d1b8d 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -1,5 +1,3 @@ -'use strict' - module.exports = appInfo => { const config = exports = {} @@ -8,6 +6,8 @@ module.exports = appInfo => { config.version = appInfo.pkg.version + config.site = appInfo.pkg.author.site + config.isLocal = appInfo.env === 'local' config.isProd = appInfo.env === 'prod' @@ -23,7 +23,7 @@ module.exports = appInfo => { config.session = { key: appInfo.name + '-token', maxAge: 60000 * 60 * 24 * 7, - signed: false + signed: true } config.userCookieKey = appInfo.name + '_userid' @@ -45,15 +45,15 @@ module.exports = appInfo => { // mongoose配置 config.mongoose = { - url: 'mongodb://127.0.0.1/node-server', - options: { + url: 'mongodb://127.0.0.1/node-server', + options: { useNewUrlParser: true, - poolSize: 20, - keepAlive: true, - autoReconnect: true, - reconnectInterval: 1000, - reconnectTries: Number.MAX_VALUE - } + poolSize: 20, + keepAlive: true, + autoReconnect: true, + reconnectInterval: 1000, + reconnectTries: Number.MAX_VALUE + } } config.redis = { @@ -79,12 +79,12 @@ module.exports = appInfo => { // 请求响应code config.codeMap = { '-1': '请求失败', - '200': '请求成功', - '401': '权限校验失败', - '403': 'Forbidden', - '404': '资源未找到', - '422': '参数校验失败', - '500': '服务器错误' + 200: '请求成功', + 401: '权限校验失败', + 403: 'Forbidden', + 404: 'URL资源未找到', + 422: '参数校验失败', + 500: '服务器错误' } config.modelValidate = { @@ -111,5 +111,11 @@ module.exports = appInfo => { } } + // 初始化管理员,默认的名称和密码,名称需要是github名称 + config.defaultAdmin = { + name: appInfo.pkg.author.name, + password: 'admin123456' + } + return config } diff --git a/config/config.local.js b/config/config.local.js index 01bb24b..a25719a 100644 --- a/config/config.local.js +++ b/config/config.local.js @@ -1,10 +1,8 @@ 'use strict' -module.exports = appInfo => { +module.exports = () => { const config = exports = {} - config.isDev = true - config.logger = { level: 'DEBUG', consoleLevel: 'DEBUG', @@ -16,5 +14,11 @@ module.exports = appInfo => { } } + // 本地开发调试用 + config.github = { + clientId: '5b4d4a7945347d0fd2e2', + clientSecret: '8771bd9ae52749cc15b0c9e2c6cb4ecd7f39d9da' + } + return config } diff --git a/config/config.prod.js b/config/config.prod.js index 28cd280..164ac18 100644 --- a/config/config.prod.js +++ b/config/config.prod.js @@ -1,4 +1,4 @@ -module.exports = appInfo => { +module.exports = () => { const config = exports = {} config.console = { @@ -7,4 +7,4 @@ module.exports = appInfo => { } return config -} \ No newline at end of file +} diff --git a/package.json b/package.json index 084c16d..073cfcd 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "description": "", "private": true, "dependencies": { + "axios": "^0.18.0", + "bcryptjs": "^2.4.3", "egg": "^2.2.1", "egg-console": "^2.0.1", "egg-mongoose": "^3.1.0", @@ -14,6 +16,7 @@ "koa-compose": "^4.1.0", "koa-is-json": "^1.0.0", "lodash": "^4.17.10", + "mongoose-paginate-v2": "^1.0.12", "validator": "^10.6.0", "zlib": "^1.0.5" }, @@ -49,6 +52,10 @@ "type": "git", "url": "git@github.com:jo0ger/node-server.git" }, - "author": "", + "author": { + "name": "jo0ger", + "email": "iamjooger@gmail.com", + "url": "https://jooger.me" + }, "license": "MIT" } diff --git a/test/app/controller/home.test.js b/test/app/controller/home.test.js index bcafc4a..9d8d590 100644 --- a/test/app/controller/home.test.js +++ b/test/app/controller/home.test.js @@ -4,18 +4,18 @@ const { app, assert } = require('egg-mock/bootstrap'); describe('test/app/controller/home.test.js', () => { - it('should assert', function* () { - const pkg = require('../../../package.json'); - assert(app.config.keys.startsWith(pkg.name)); + it('should assert', function* () { + const pkg = require('../../../package.json'); + assert(app.config.keys.startsWith(pkg.name)); - // const ctx = app.mockContext({}); - // yield ctx.service.xx(); - }); + // const ctx = app.mockContext({}); + // yield ctx.service.xx(); + }); - it('should GET /', () => { - return app.httpRequest() - .get('/') - .expect('hi, egg') - .expect(200); - }); + it('should GET /', () => { + return app.httpRequest() + .get('/') + .expect('hi, egg') + .expect(200); + }); }); From 237eaf91ac2b3b9c199766d8e06ee91aab5647bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 24 Aug 2018 20:01:13 +0800 Subject: [PATCH 116/208] update: article controller done --- app/controller/article.js | 28 +++++++-- app/extend/application.js | 14 ++++- app/extend/context.js | 9 +++ app/model/article.js | 38 ++++++++++-- app/router.js | 13 ++++ app/service/article.js | 127 +++++++++++++++++++++++++++++++++++--- app/service/auth.js | 22 +++++-- app/service/category.js | 6 +- app/service/proxy.js | 2 +- app/service/setting.js | 3 +- app/service/tag.js | 3 +- app/service/user.js | 18 +----- app/utils/markdown.js | 105 +++++++++++++++++++++++++++++++ app/utils/share.js | 4 ++ config/config.default.js | 14 ++--- package.json | 2 + 16 files changed, 352 insertions(+), 56 deletions(-) create mode 100644 app/utils/markdown.js diff --git a/app/controller/article.js b/app/controller/article.js index 35f168b..1091320 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -21,13 +21,33 @@ module.exports = class ArticleController extends Controller { : ctx.fail('文章详情获取失败') } - async create () {} + async create () { + const { ctx } = this + const data = await this.service.article.create() + data + ? ctx.success(data, '文章创建成功') + : ctx.fail('文章创建失败') + } - async update () {} + async update () { + const { ctx } = this + const data = await this.service.article.update() + data + ? ctx.success(data, '文章更新成功') + : ctx.fail('文章更新失败') + } async delete () {} - async like () {} + async like () { + const { ctx } = this + const data = await this.service.article.update() + data + ? ctx.success(data, '文章点赞成功') + : ctx.fail('文章点赞失败') + } - async archives () {} + async archives () { + this.ctx.success(await this.service.article.archives(), '归档获取成功') + } } diff --git a/app/extend/application.js b/app/extend/application.js index b77b0e7..d29d751 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -2,16 +2,26 @@ const mongoosePaginate = require('mongoose-paginate-v2') module.exports = { // model schema处理 - processSchema (schema, paginate) { + processSchema (schema, options = {}, middlewares = {}) { if (!schema) { return null } schema.set('versionKey', false) schema.set('toObject', { getters: true }) schema.set('toJSON', { getters: true, virtuals: false }) - if (paginate) { + if (options.paginate) { schema.plugin(mongoosePaginate) } + schema.pre('findOneAndUpdate', function (next) { + this._update.updatedAt = Date.now() + next() + }) + Object.keys(middlewares).forEach(key => { + const fns = middlewares[key] + Object.keys(fns).forEach(action => { + schema[key](action, fns[action]) + }) + }) return schema } } diff --git a/app/extend/context.js b/app/extend/context.js index 9bc7c76..c4f3bd7 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -6,5 +6,14 @@ module.exports = { required } }, data) + }, + validateBody (rules, body) { + this.validate(rules, body || this.request.body) + return Object.keys(rules).reduce((res, key) => { + if (body.hasOwnProperty(key)) { + res[key] = body[key] + } + return res + }, {}) } } diff --git a/app/model/article.js b/app/model/article.js index 4829827..1a8720c 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -17,7 +17,7 @@ module.exports = app => { // 文章原始markdown内容 content: { type: String, required: true, validate: /\S+/ }, // markdown渲染后的htmln内容 - renderedContent: { type: String, required: true, validate: /\S+/ }, + renderedContent: { type: String, required: false, validate: /\S+/ }, // 分类 category: { type: Schema.Types.ObjectId, ref: 'Category' }, // 标签 @@ -32,8 +32,6 @@ module.exports = app => { return Object.values(articleValidateConfig.state.optional).includes(val) } }, - // 永久链接 - permalink: { type: String, validate: /\S+/ }, // 创建日期 createdAt: { type: Date, default: Date.now }, // 更新日期 @@ -48,5 +46,37 @@ module.exports = app => { } }) - return mongoose.model('Article', app.processSchema(ArticleSchema, true)) + return mongoose.model('Article', app.processSchema(ArticleSchema, { + paginate: true + }, { + pre: { + save (next) { + this.renderedContent = app.utils.markdown.render(this.content) + next() + }, + async findOneAndUpdate () { + // HACK: 这里this指向的是query,而不是这个model + delete this._update.updatedAt + const { content, state } = this._update + const find = await this.findOne() + if (find) { + if (content !== find.content) { + this._update.renderedContent = app.utils.markdown.render(content) + } + if (['title', 'content'].some(key => this._update[key] !== find[key])) { + // 只有内容和标题不一样时才更新updatedAt + this._update.updatedAt = Date.now() + } + if (state !== find.state) { + // 更新发布日期 + if (state === articleValidateConfig.state.optional.PUBLISH) { + this._update.publishedAt = Date.now() + } else { + this._update.publishedAt = find.updatedAt + } + } + } + } + } + })) } diff --git a/app/router.js b/app/router.js index 92763a9..545ad14 100644 --- a/app/router.js +++ b/app/router.js @@ -26,6 +26,9 @@ function frontend (app) { // Article router.get('/articles', controller.article.list) + router.get('/articles/archives', controller.article.archives) + router.get('/articles/:id', controller.article.item) + router.patch('/articles/:id', controller.article.like) // Category router.get('/categories', controller.category.list) @@ -48,6 +51,16 @@ function backend (app) { const { router, controller, middlewares } = app const auth = middlewares.auth(app) + // Article + router.get('/backend/articles', auth, controller.article.list) + router.get('/backend/articles/archives', auth, controller.article.archives) + router.get('/backend/articles/:id', auth, controller.article.item) + router.post('/backend/articles', auth, controller.article.create) + router.put('/backend/articles/:id', auth, controller.article.update) + router.patch('/backend/articles/:id', auth, controller.article.update) + router.delete('/backend/articles/:id', auth, controller.article.delete) + router.patch('/backend/articles/:id/like', auth, controller.article.like) + // Category router.get('/backend/categories', auth, controller.category.list) router.get('/backend/categories/:id', auth, controller.category.item) diff --git a/app/service/article.js b/app/service/article.js index 5f8176c..382159b 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -27,6 +27,28 @@ module.exports = class ArticleService extends ProxyService { item: { // 后台用,只获取当前文章内容,不获取相关文章和上下篇文章 single: { type: 'boolean', required: false } + }, + create: { + title: { type: 'string', required: true }, + content: { type: 'string', required: true }, + description: { type: 'string', required: false }, + keywords: { type: 'array', required: false }, + category: { type: 'objectId', required: false }, + tag: { type: 'array', required: false, itemType: 'objectId' }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + thumb: { type: 'url', required: false }, + createdAt: { type: 'dateTime', required: false } + }, + update: { + title: { type: 'string', required: false }, + content: { type: 'string', required: false }, + description: { type: 'string', required: false }, + keywords: { type: 'array', required: false }, + category: { type: 'objectId', required: false }, + tag: { type: 'array', required: false, itemType: 'objectId' }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + thumb: { type: 'url', required: false }, + createdAt: { type: 'dateTime', required: false } } } } @@ -135,11 +157,11 @@ module.exports = class ArticleService extends ProxyService { let query = null // 只有前台博客访问文章的时候pv才+1 if (!ctx._isAuthenticated) { - query = this.updateOne({ _id: params.id, state: this.config.modelValidate.article.optional.PUBLISH }, { $inc: { 'meta.pvs': 1 } }).select('-content') + query = this.updateOne({ _id: params.id, state: this.config.modelValidate.article.state.optional.PUBLISH }, { $inc: { 'meta.pvs': 1 } }).select('-content') } else { query = this.findById(params.id) } - let data = await query.populate([ + const data = await query.populate([ { path: 'category', select: 'name description extends' @@ -148,9 +170,9 @@ module.exports = class ArticleService extends ProxyService { select: 'name description extends' } ]).exec() - if (!ctx.query.single) { + + if (data && !ctx.query.single) { // 获取相关文章和上下篇文章 - data = data.toObject() const [related, adjacent] = await Promise.all([ this.getRelatedArticles(data), this.getAdjacentArticles(data) @@ -161,9 +183,28 @@ module.exports = class ArticleService extends ProxyService { return data } - async create () {} + async create () { + const body = this.ctx.validateBody(this.rules.create) + if (body.createdAt) { + body.createdAt = new Date(body.createdAt) + } + let data = await this.newAndSave(body) + if (data && data.length) { + data = data[0].toObject() + } + return data + } - async update () {} + async update () { + const { ctx } = this + const { params } = ctx + ctx.validateObjectId(params) + const body = ctx.validateBody(this.rules.update) + if (body.createdAt) { + body.createdAt = new Date(body.createdAt) + } + return await this.updateById(params.id, body).populate('category tag').exec() + } async delete () { const { ctx } = this @@ -173,9 +214,77 @@ module.exports = class ArticleService extends ProxyService { return data && data.ok && data.n } - async like () {} + async like () { + const { ctx } = this + const { params } = ctx + ctx.validateObjectId(params) + return await this.updateById(params.id, { + $inc: { + 'meta.ups': 1 + } + }) + } - async archives () {} + async archives () { + const $match = {} + const $project = { + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + title: 1, + createdAt: 1 + } + if (!this.ctx._isAuthenticated) { + $match.state = 1 + } else { + $project.state = 1 + } + let data = await this.aggregate([ + { $match }, + { $sort: { createdAt: 1 } }, + { $project }, + { + $group: { + _id: { + year: '$year', + month: '$month' + }, + articles: { + $push: { + title: '$title', + _id: '$_id', + createdAt: '$createdAt', + state: '$state' + } + } + } + } + ]) + let count = 0 + if (data && data.length) { + data = [...new Set(data.map(item => item._id.year))].map(year => { + const months = [] + data.forEach(item => { + const { _id, articles } = item + if (year === _id.year) { + count += articles.length + months.push({ + month: _id.month, + monthStr: this.app.utils.share.getMonthFromNum(_id.month), + articles + }) + } + }) + return { + year, + months + } + }) + } + return { + count, + list: data || [] + } + } // 根据标签获取相关文章 async getRelatedArticles (data) { @@ -205,7 +314,7 @@ module.exports = class ArticleService extends ProxyService { const query = {} // 如果未通过权限校验,将文章状态重置为1 if (!ctx._isAuthenticated) { - query.state = this.config.modelValidate.article.optional.PUBLISH + query.state = this.config.modelValidate.article.state.optional.PUBLISH } const prev = await this.service.article.findOne(query) .select('title createdAt publishedAt thumb category') diff --git a/app/service/auth.js b/app/service/auth.js index ea7203d..f312966 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -20,14 +20,17 @@ module.exports = class AuthService extends Service { slogan: { type: 'string', required: false }, company: { type: 'string', required: false }, location: { type: 'string', required: false } + }, + password: { + password: { type: 'string', required: true }, + oldPassword: { type: 'string', required: true } } } } async login () { const { ctx } = this - const { body } = ctx.request - ctx.validate(this.rules.login, body) + const body = this.ctx.validateBody(this.rules.login) const user = await this.service.user.findOne({ name: body.username }).exec() if (!user) { return ctx.fail('用户不存在') @@ -86,11 +89,22 @@ module.exports = class AuthService extends Service { async update () { const { ctx } = this - const { body } = ctx.request - ctx.validate(this.rules.update, body) + const body = this.ctx.validateBody(this.rules.update) return await this.service.user.updateById(ctx._user_id, body) } + async password () { + const { ctx } = this + const body = this.ctx.validateBody(this.rules.password) + const verify = this.app.utils.encode.bcompare(body.oldPassword, ctx._user.password) + if (!verify) { + ctx.throw(200, '原密码错误') + } + return await this.updateById(ctx._user._id, { + password: this.app.utils.encode.bhash(body.password) + }).exec() + } + async seed () { const ADMIN = this.config.modelValidate.user.role.optional.ADMIN const exist = await this.service.user.findOne({ role: ADMIN }).exec() diff --git a/app/service/category.js b/app/service/category.js index 68998cd..3e27976 100644 --- a/app/service/category.js +++ b/app/service/category.js @@ -98,8 +98,7 @@ module.exports = class CategoryService extends ProxyService { async create () { const { ctx } = this - const { body } = ctx.request - ctx.validate(this.rules.create, body) + const body = this.ctx.validateBody(this.rules.create) const exists = await this.find({ name: body.name }).exec() if (exists && exists.length) { ctx.throw(200, '分类已经存在') @@ -110,9 +109,8 @@ module.exports = class CategoryService extends ProxyService { async update () { const { ctx } = this const { params } = ctx - const { body } = ctx.request ctx.validateObjectId(params) - ctx.validate(this.rules.update, body) + const body = this.ctx.validateBody(this.rules.update) return await this.updateById(params.id, body).exec() } diff --git a/app/service/proxy.js b/app/service/proxy.js index 2cb88c4..895c993 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -9,7 +9,7 @@ module.exports = class ProxyService extends Service { if (!Array.isArray(docs)) { docs = [docs] } - return this.model.insertMany(docs) + return this.model.create(docs) } paginate (query, opt = {}) { diff --git a/app/service/setting.js b/app/service/setting.js index 382e930..80900dc 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -57,8 +57,7 @@ module.exports = class SettingService extends ProxyService { async create (payload) { const { ctx } = this - const body = payload || ctx.request.body - ctx.validate(this.rules.create, body) + const body = this.ctx.validateBody(this.rules.create, payload) const exist = await this.findOne().exec() if (exist) { ctx.throw(200, '分类已经存在') diff --git a/app/service/tag.js b/app/service/tag.js index 2593150..9dcf1a8 100644 --- a/app/service/tag.js +++ b/app/service/tag.js @@ -108,9 +108,8 @@ module.exports = class TagService extends ProxyService { async update () { const { ctx } = this const { params } = ctx - const { body } = ctx.request ctx.validateObjectId(params) - ctx.validate(this.rules.update, body) + const body = this.ctx.validateBody(this.rules.create) return await this.updateById(params.id, body).exec() } diff --git a/app/service/user.js b/app/service/user.js index c1d9ca1..7a4fcb8 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -11,10 +11,7 @@ module.exports = class UserService extends ProxyService { get rules () { return { - password: { - password: { type: 'string', required: true }, - oldPassword: { type: 'string', required: true } - } + // TODO: } } @@ -40,17 +37,4 @@ module.exports = class UserService extends ProxyService { } return await this.findById(params.id).select(select).exec() } - - async password () { - const { ctx } = this - const { body } = ctx.request - ctx.validate(this.rules.password, body) - const verify = this.app.utils.encode.bcompare(body.oldPassword, ctx._user.password) - if (!verify) { - ctx.throw(200, '原密码错误') - } - return await this.updateById(ctx._user._id, { - password: this.app.utils.encode.bhash(body.password) - }).exec() - } } diff --git a/app/utils/markdown.js b/app/utils/markdown.js new file mode 100644 index 0000000..b279bfd --- /dev/null +++ b/app/utils/markdown.js @@ -0,0 +1,105 @@ +const marked = require('marked') +const highlight = require('highlight.js') +const { randomString } = require('./encode') + +const languages = ['xml', 'bash', 'css', 'markdown', 'http', 'java', 'javascript', 'json', 'makefile', 'nginx', 'python', 'scss', 'sql', 'stylus'] +highlight.registerLanguage('xml', require('highlight.js/lib/languages/xml')) +highlight.registerLanguage('bash', require('highlight.js/lib/languages/bash')) +highlight.registerLanguage('css', require('highlight.js/lib/languages/css')) +highlight.registerLanguage('markdown', require('highlight.js/lib/languages/markdown')) +highlight.registerLanguage('http', require('highlight.js/lib/languages/http')) +highlight.registerLanguage('java', require('highlight.js/lib/languages/java')) +highlight.registerLanguage('javascript', require('highlight.js/lib/languages/javascript')) +highlight.registerLanguage('typescript', require('highlight.js/lib/languages/typescript')) +highlight.registerLanguage('json', require('highlight.js/lib/languages/json')) +highlight.registerLanguage('makefile', require('highlight.js/lib/languages/makefile')) +highlight.registerLanguage('nginx', require('highlight.js/lib/languages/nginx')) +highlight.registerLanguage('python', require('highlight.js/lib/languages/python')) +highlight.registerLanguage('scss', require('highlight.js/lib/languages/scss')) +highlight.registerLanguage('sql', require('highlight.js/lib/languages/sql')) +highlight.registerLanguage('stylus', require('highlight.js/lib/languages/stylus')) +highlight.configure({ + classPrefix: '' // don't append class prefix +}) + +const renderer = new marked.Renderer() + +renderer.heading = function (text, level) { + return `${text}` +} + +renderer.link = function (href, title, text) { + const isOrigin = href.indexOf('jooger.me') > -1 + const isImage = /(/gi.test(text) + return ` + ${text} + `.replace(/\s+/g, ' ').replace('\n', '') +} + +renderer.image = function (href, title, text) { + return ` + ${text || title || href} + `.replace(/\s+/g, ' ').replace('\n', '') +} + +renderer.code = function (code, lang) { + if (this.options.highlight) { + const out = this.options.highlight(code, lang) + if (out != null && out !== code) { + code = out + } + } + + const lineCode = code.split('\n') + const codeWrapper = lineCode.map((line, index) => `${line}${index !== lineCode.length - 1 ? '
' : ''}`.replace(/\s+/g, ' ')).join('') + + if (!lang) { + return '
' +
+            codeWrapper +
+            '\n
' + } + + return '
' +
+        codeWrapper +
+        '\n
\n' +} + +marked.setOptions({ + renderer, + gfm: true, + pedantic: false, + sanitize: false, + tables: true, + breaks: true, + smartLists: true, + smartypants: true, + highlight (code, lang) { + if (languages.indexOf(lang) < 0) { + return highlight.highlightAuto(code).value + } + return highlight.highlight(lang, code).value + } +}) + +function escape (html, encode) { + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +exports.render = marked diff --git a/app/utils/share.js b/app/utils/share.js index 110c91a..886d685 100644 --- a/app/utils/share.js +++ b/app/utils/share.js @@ -22,3 +22,7 @@ exports.getDocsPaginationData = docs => { } } } + +exports.getMonthFromNum = (num = 1) => { + return ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][num - 1] || '' +} diff --git a/config/config.default.js b/config/config.default.js index 91d1b8d..b5567cd 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -91,21 +91,21 @@ module.exports = appInfo => { article: { // 文章状态 ( 0 草稿(默认) | 1 已发布 ) state: { - default: 0, + default: '0', optional: { - DRAFT: 0, - PUBLISH: 1 + DRAFT: '0', + PUBLISH: '1' } } }, user: { // 角色 0 管理员 | 1 普通用户 | 2 gayhub用户,不能更改 role: { - default: 1, + default: '1', optional: { - ADMIN: 0, - NORMAL: 1, - GAYHUB: 2 + ADMIN: '0', + NORMAL: '1', + GAYHUB: '2' } } } diff --git a/package.json b/package.json index 073cfcd..e512d15 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "egg-redis": "^2.0.0", "egg-scripts": "^2.5.0", "egg-validate": "^1.1.1", + "highlight.js": "^9.12.0", "jsonwebtoken": "^8.3.0", "koa-compose": "^4.1.0", "koa-is-json": "^1.0.0", "lodash": "^4.17.10", + "marked": "^0.5.0", "mongoose-paginate-v2": "^1.0.12", "validator": "^10.6.0", "zlib": "^1.0.5" From 31576d3f642891c7e37974e214d8b4de1770f77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 24 Aug 2018 20:46:57 +0800 Subject: [PATCH 117/208] update: config --- app/controller/article.js | 8 ++++++- app/extend/context.js | 36 +++++++++++++++++++++++------- app/model/article.js | 4 +--- app/model/comment.js | 21 +++++++++++++---- app/model/user.js | 4 +--- app/router.js | 2 +- app/service/article.js | 8 +++---- app/service/auth.js | 4 ++-- app/service/category.js | 6 ++--- app/service/comment.js | 32 ++++++++++++++++++++++++++ app/service/{util.js => common.js} | 0 app/service/setting.js | 2 +- app/service/user.js | 2 +- config/config.default.js | 29 ++++++++++++++++++++++++ package.json | 1 + 15 files changed, 128 insertions(+), 31 deletions(-) create mode 100644 app/service/comment.js rename app/service/{util.js => common.js} (100%) diff --git a/app/controller/article.js b/app/controller/article.js index 1091320..d2777d1 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -37,7 +37,13 @@ module.exports = class ArticleController extends Controller { : ctx.fail('文章更新失败') } - async delete () {} + async delete () { + const { ctx } = this + const data = await this.service.article.delete() + data + ? ctx.success('文章删除成功') + : ctx.fail('文章删除失败') + } async like () { const { ctx } = this diff --git a/app/extend/context.js b/app/extend/context.js index c4f3bd7..3967eaa 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -1,19 +1,39 @@ +const geoip = require('geoip-lite') + module.exports = { - validateObjectId (data, required) { - return this.validate({ - id: { - type: 'objectId', - required - } - }, data) + validateParams (rules) { + return this.validate(rules, this.params) }, validateBody (rules, body) { - this.validate(rules, body || this.request.body) + body = body || this.request.body + this.validate(rules, body) return Object.keys(rules).reduce((res, key) => { if (body.hasOwnProperty(key)) { res[key] = body[key] } return res }, {}) + }, + validateParamsObjectId () { + return this.validateParams({ + id: { + type: 'objectId', + required: true + } + }) + }, + getLocation () { + const req = this.req + const ip = (req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + req.connection.socket.remoteAddress || + req.ip || + req.ips[0] || '').replace('::ffff:', '') + return { + ip, + location: geoip.lookup(ip) || {} + } } } diff --git a/app/model/article.js b/app/model/article.js index 1a8720c..05adc31 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -28,9 +28,7 @@ module.exports = app => { state: { type: Number, default: articleValidateConfig.state.default, - validate: val => { - return Object.values(articleValidateConfig.state.optional).includes(val) - } + validate: val => Object.values(articleValidateConfig.state.optional).includes(val) }, // 创建日期 createdAt: { type: Date, default: Date.now }, diff --git a/app/model/comment.js b/app/model/comment.js index b5ff8a0..ed0e6d7 100644 --- a/app/model/comment.js +++ b/app/model/comment.js @@ -1,6 +1,7 @@ module.exports = app => { - const { mongoose } = app + const { mongoose, config } = app const { Schema } = mongoose + const commentValidateConfig = config.modelValidate.comment const CommentSchema = new Schema({ // ******* 评论通用项 ************ @@ -13,7 +14,11 @@ module.exports = app => { // marked渲染后的内容 renderedContent: { type: String, required: true, validate: /\S+/ }, // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 - state: { type: Number, default: 1 }, + state: { + type: Number, + default: commentValidateConfig.state.default, + validate: val => Object.values(commentValidateConfig.state.optional).includes(val) + }, // Akismet判定是否是垃圾评论,方便后台check spam: { type: Boolean, default: false }, // 评论发布者 @@ -21,9 +26,17 @@ module.exports = app => { // 点赞数 ups: { type: Number, default: 0, validate: /^\d*$/ }, // 是否置顶 0 否 | 1 是 - sticky: { type: Number, default: 0 }, + sticky: { + type: Number, + default: commentValidateConfig.sticky.default, + validate: val => Object.values(commentValidateConfig.sticky.optional).includes(val) + }, // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) - type: { type: Number, default: 0 }, + type: { + type: Number, + default: commentValidateConfig.type.default, + validate: val => Object.values(commentValidateConfig.type.optional).includes(val) + }, // type为0时此项存在 article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, meta: { diff --git a/app/model/user.js b/app/model/user.js index 00590a9..04b08fb 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -18,9 +18,7 @@ module.exports = app => { role: { type: Number, default: userValidateConfig.role.default, - validate: val => { - return Object.values(userValidateConfig.role.optional).includes(val) - } + validate: val => Object.values(userValidateConfig.role.optional).includes(val) }, // role = 0的时候才有该项 password: { type: String }, diff --git a/app/router.js b/app/router.js index 545ad14..2bada04 100644 --- a/app/router.js +++ b/app/router.js @@ -58,8 +58,8 @@ function backend (app) { router.post('/backend/articles', auth, controller.article.create) router.put('/backend/articles/:id', auth, controller.article.update) router.patch('/backend/articles/:id', auth, controller.article.update) - router.delete('/backend/articles/:id', auth, controller.article.delete) router.patch('/backend/articles/:id/like', auth, controller.article.like) + router.delete('/backend/articles/:id', auth, controller.article.delete) // Category router.get('/backend/categories', auth, controller.category.list) diff --git a/app/service/article.js b/app/service/article.js index 382159b..d123f5a 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -152,7 +152,7 @@ module.exports = class ArticleService extends ProxyService { async item () { const { ctx } = this const { params } = ctx - ctx.validateObjectId(params) + ctx.validateParamsObjectId() ctx.validate(this.rules.item, ctx.query) let query = null // 只有前台博客访问文章的时候pv才+1 @@ -198,7 +198,7 @@ module.exports = class ArticleService extends ProxyService { async update () { const { ctx } = this const { params } = ctx - ctx.validateObjectId(params) + ctx.validateParamsObjectId() const body = ctx.validateBody(this.rules.update) if (body.createdAt) { body.createdAt = new Date(body.createdAt) @@ -209,7 +209,7 @@ module.exports = class ArticleService extends ProxyService { async delete () { const { ctx } = this const { params } = ctx - ctx.validateObjectId(params) + ctx.validateParamsObjectId() const data = await this.deleteById(params.id).exec() return data && data.ok && data.n } @@ -217,7 +217,7 @@ module.exports = class ArticleService extends ProxyService { async like () { const { ctx } = this const { params } = ctx - ctx.validateObjectId(params) + ctx.validateParamsObjectId() return await this.updateById(params.id, { $inc: { 'meta.ups': 1 diff --git a/app/service/auth.js b/app/service/auth.js index f312966..fafe122 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -116,7 +116,7 @@ module.exports = class AuthService extends Service { async create (name) { const ADMIN = this.config.modelValidate.user.role.optional.ADMIN const defaultAdmin = this.config.defaultAdmin - const admin = await this.service.util.getGithubUserInfo(name || defaultAdmin.name) + const admin = await this.service.common.getGithubUserInfo(name || defaultAdmin.name) if (!admin) { return this.logger.warn('管理员创建失败') } @@ -127,7 +127,7 @@ module.exports = class AuthService extends Service { password: this.app.utils.encode.bhash(defaultAdmin.password), slogan: admin.bio, site: admin.blog || admin.url, - avatar: this.service.util.proxyUrl(admin.avatar_url), + avatar: this.service.common.proxyUrl(admin.avatar_url), company: admin.company, location: admin.location, github: { diff --git a/app/service/category.js b/app/service/category.js index 3e27976..f652f34 100644 --- a/app/service/category.js +++ b/app/service/category.js @@ -83,7 +83,7 @@ module.exports = class CategoryService extends ProxyService { async item () { const { ctx } = this const { params } = ctx - ctx.validateObjectId(params) + ctx.validateParamsObjectId() let data = await this.findById(params.id).exec() if (data) { data = data.toObject() @@ -109,7 +109,7 @@ module.exports = class CategoryService extends ProxyService { async update () { const { ctx } = this const { params } = ctx - ctx.validateObjectId(params) + ctx.validateParamsObjectId() const body = this.ctx.validateBody(this.rules.update) return await this.updateById(params.id, body).exec() } @@ -117,7 +117,7 @@ module.exports = class CategoryService extends ProxyService { async delete () { const { ctx } = this const { params } = ctx - ctx.validateObjectId(params) + ctx.validateParamsObjectId() const articles = await this.service.article.find({ category: params.id }).exec() if (articles && articles.length) { ctx.throw(200, '该分类下有文章,不能删除') diff --git a/app/service/comment.js b/app/service/comment.js new file mode 100644 index 0000000..8bd3924 --- /dev/null +++ b/app/service/comment.js @@ -0,0 +1,32 @@ +/** + * @desc Comment Services + */ + +const ProxyService = require('./proxy') + +module.exports = class CommentService extends ProxyService { + get model () { + return this.app.model.Comment + } + + get rules () { + return { + // todo + } + } + + async list () {} + + async item () {} + + async create () {} + + async update () {} + + async delete () { + const { ctx } = this + ctx.validateParamsObjectId() + } + + async like () {} +} diff --git a/app/service/util.js b/app/service/common.js similarity index 100% rename from app/service/util.js rename to app/service/common.js diff --git a/app/service/setting.js b/app/service/setting.js index 80900dc..55ac252 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -72,7 +72,7 @@ module.exports = class SettingService extends ProxyService { if (!payload) return } // 更新友链 - payload.site.links = await this.service.util.generateLinks(payload.site.links) + payload.site.links = await this.service.common.generateLinks(payload.site.links) const data = await this.updateOne({}, payload).exec() if (data) { this.logger.info('Setting更新成功') diff --git a/app/service/user.js b/app/service/user.js index 7a4fcb8..3ec1a9b 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -30,7 +30,7 @@ module.exports = class UserService extends ProxyService { async item () { const { ctx } = this const { params } = ctx - ctx.validateObjectId(params) + ctx.validateParamsObjectId() let select = '-password' if (!ctx._isAuthenticated) { select += ' -createdAt -updatedAt -github' diff --git a/config/config.default.js b/config/config.default.js index b5567cd..8c9b459 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -108,6 +108,35 @@ module.exports = appInfo => { GAYHUB: '2' } } + }, + comment: { + // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 + state: { + default: '1', + optional: { + SPAM: '-2', + DELETED: '-1', + AUDITING: '0', + PASS: '1' + } + }, + // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) + type: { + default: '0', + optional: { + COMMENT: '0', + MESSAGE: '1', + OTHER: '2' + } + }, + // 是否置顶 0 否 | 1 是 + sticky: { + default: '0', + optional: { + NORMAL: '0', + STICKY: '1' + } + } } } diff --git a/package.json b/package.json index e512d15..bd2c994 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "egg-redis": "^2.0.0", "egg-scripts": "^2.5.0", "egg-validate": "^1.1.1", + "geoip-lite": "^1.3.2", "highlight.js": "^9.12.0", "jsonwebtoken": "^8.3.0", "koa-compose": "^4.1.0", From c00413f6e070a3938084a8277f50ed7abcf0d8c9 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 26 Aug 2018 16:17:55 +0800 Subject: [PATCH 118/208] update: add akismet plugin --- app/lib/plugin/egg-akismet/agent.js | 3 + app/lib/plugin/egg-akismet/app.js | 3 + .../egg-akismet/config/config.default.js | 5 + app/lib/plugin/egg-akismet/lib/akismet.js | 120 ++++++++++++++++++ app/lib/plugin/egg-akismet/package.json | 6 + app/model/article.js | 2 +- app/model/comment.js | 12 +- app/model/user.js | 2 +- app/service/article.js | 2 +- app/service/comment.js | 44 ++++++- app/service/user.js | 47 +++++++ app/utils/gravatar.js | 21 +++ config/config.default.js | 26 ++-- config/plugin.js | 7 + package.json | 3 + 15 files changed, 281 insertions(+), 22 deletions(-) create mode 100644 app/lib/plugin/egg-akismet/agent.js create mode 100644 app/lib/plugin/egg-akismet/app.js create mode 100644 app/lib/plugin/egg-akismet/config/config.default.js create mode 100644 app/lib/plugin/egg-akismet/lib/akismet.js create mode 100644 app/lib/plugin/egg-akismet/package.json create mode 100644 app/utils/gravatar.js diff --git a/app/lib/plugin/egg-akismet/agent.js b/app/lib/plugin/egg-akismet/agent.js new file mode 100644 index 0000000..b24b820 --- /dev/null +++ b/app/lib/plugin/egg-akismet/agent.js @@ -0,0 +1,3 @@ +module.exports = agent => { + if (agent.config.akismet.agent) require('./lib/akismet')(agent); +}; diff --git a/app/lib/plugin/egg-akismet/app.js b/app/lib/plugin/egg-akismet/app.js new file mode 100644 index 0000000..2f57c8f --- /dev/null +++ b/app/lib/plugin/egg-akismet/app.js @@ -0,0 +1,3 @@ +module.exports = app => { + if (app.config.akismet.app) require('./lib/akismet')(app) +} \ No newline at end of file diff --git a/app/lib/plugin/egg-akismet/config/config.default.js b/app/lib/plugin/egg-akismet/config/config.default.js new file mode 100644 index 0000000..e5690a3 --- /dev/null +++ b/app/lib/plugin/egg-akismet/config/config.default.js @@ -0,0 +1,5 @@ +exports.akismet = { + app: true, + agent: false, + client: {} +} diff --git a/app/lib/plugin/egg-akismet/lib/akismet.js b/app/lib/plugin/egg-akismet/lib/akismet.js new file mode 100644 index 0000000..9985531 --- /dev/null +++ b/app/lib/plugin/egg-akismet/lib/akismet.js @@ -0,0 +1,120 @@ +const akismet = require('akismet-api') + +module.exports = app => { + app.addSingleton('akismet', createClient) + app.beforeStart(async () => { + const { valid, error } = await app.akismet.verifyKey() + if (valid) { + app.coreLogger.info('[egg-akismet] 服务启动成功') + } else { + app.coreLogger.error(`[egg-akismet] 服务启动失败:${error}`) + } + }) +} + +function createClient (config, app) { + return new AkismetClient(config, app) +} + +// Akismet apikey是否验证通过 +let isValidKey = false + +/** + * @desc Akismet Client Class + * @param {String} [required] key Akismet apikey + * @param {String} [required] blog Akismet blog + */ +class AkismetClient { + constructor (config, app) { + this.config = config + this.app = app + this.initClient() + } + + initClient () { + this.client = akismet.client(this.config) + } + + async verifyKey () { + let valid = true + let error = '' + if (!isValidKey) { + const v = await this.client.verifyKey().catch(err => { + error = 'Apikey验证失败,错误:' + err.message + }) + valid = v + if (v) { + isValidKey = true + } else { + error = '无效的Apikey' + this.client = null + } + } + return { valid, error } + } + + // 检测是否是spam + checkSpam (opt = {}) { + this.app.coreLogger.info('验证评论中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.checkSpam(opt, (err, spam) => { + if (err) { + this.app.coreLogger.error('[egg-akismet] 评论验证失败,将跳过Spam验证,错误:', err.message) + return reject(false) + } + if (spam) { + this.app.coreLogger.warn('[egg-akismet] 评论验证不通过,疑似垃圾评论') + resolve(true) + } else { + this.app.coreLogger.success('[egg-akismet] 评论验证通过') + resolve(false) + } + }) + } else { + this.app.coreLogger.warn('[egg-akismet] Apikey未认证,将跳过Spam验证') + resolve(false) + } + }) + } + + // 提交被误检为spam的正常评论 + submitSpam (opt = {}) { + this.app.coreLogger.info('[egg-akismet] 误检Spam垃圾评论报告提交中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.submitSpam(opt, err => { + if (err) { + this.app.coreLogger.error('[egg-akismet] 误检Spam垃圾评论报告提交失败') + return reject(err) + } + this.app.coreLogger.success('[egg-akismet] 误检Spam垃圾评论报告提交成功') + resolve() + }) + } else { + this.app.coreLogger.warn('[egg-akismet] Apikey未认证,误检Spam垃圾评论报告提交失败') + resolve() + } + }) + } + + // 提交被误检为正常评论的spam + submitHam (opt = {}) { + this.app.coreLogger.info('[egg-akismet] 误检正常评论报告提交中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.submitSpam(opt, err => { + if (err) { + this.app.coreLogger.error('[egg-akismet] 误检正常评论报告提交失败') + return reject(err) + } + this.app.coreLogger.success('[egg-akismet] 误检正常评论报告提交成功') + resolve() + }) + } else { + this.app.coreLogger.warn('[egg-akismet] Apikey未认证,误检正常评论报告提交失败') + resolve() + } + }) + } +} diff --git a/app/lib/plugin/egg-akismet/package.json b/app/lib/plugin/egg-akismet/package.json new file mode 100644 index 0000000..7ccbdbf --- /dev/null +++ b/app/lib/plugin/egg-akismet/package.json @@ -0,0 +1,6 @@ +{ + "name": "egg-akismet", + "eggPlugin": { + "name": "akismet" + } +} diff --git a/app/model/article.js b/app/model/article.js index 05adc31..5c188ae 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -26,7 +26,7 @@ module.exports = app => { thumb: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, // 文章状态 ( 0 草稿 | 1 已发布 ) state: { - type: Number, + type: String, default: articleValidateConfig.state.default, validate: val => Object.values(articleValidateConfig.state.optional).includes(val) }, diff --git a/app/model/comment.js b/app/model/comment.js index ed0e6d7..6530cd1 100644 --- a/app/model/comment.js +++ b/app/model/comment.js @@ -15,7 +15,7 @@ module.exports = app => { renderedContent: { type: String, required: true, validate: /\S+/ }, // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 state: { - type: Number, + type: String, default: commentValidateConfig.state.default, validate: val => Object.values(commentValidateConfig.state.optional).includes(val) }, @@ -25,15 +25,11 @@ module.exports = app => { author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, // 点赞数 ups: { type: Number, default: 0, validate: /^\d*$/ }, - // 是否置顶 0 否 | 1 是 - sticky: { - type: Number, - default: commentValidateConfig.sticky.default, - validate: val => Object.values(commentValidateConfig.sticky.optional).includes(val) - }, + // 是否置顶 + sticky: { type: Boolean, default: false }, // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) type: { - type: Number, + type: String, default: commentValidateConfig.type.default, validate: val => Object.values(commentValidateConfig.type.optional).includes(val) }, diff --git a/app/model/user.js b/app/model/user.js index 04b08fb..295e4a7 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -16,7 +16,7 @@ module.exports = app => { description: { type: String, default: '' }, // 角色 0 管理员 | 1 普通用户 | 2 github用户,不能更改 role: { - type: Number, + type: String, default: userValidateConfig.role.default, validate: val => Object.values(userValidateConfig.role.optional).includes(val) }, diff --git a/app/service/article.js b/app/service/article.js index d123f5a..4416027 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -305,7 +305,7 @@ module.exports = class ArticleService extends ProxyService { this.logger.error('相关文章查询失败,错误:' + err.message) return null }) - return articles && articles.slice(0, 10) || null + return articles && articles.slice(0, this.config.limit.relatedArticleLimit) || null } // 获取相邻的文章 diff --git a/app/service/comment.js b/app/service/comment.js index 8bd3924..e4d9e14 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -11,7 +11,16 @@ module.exports = class CommentService extends ProxyService { get rules () { return { - // todo + create: { + content: { type: 'string', required: true }, + state: { type: 'enum', values: this.config.modelValidate.comment.state.optional, required: false }, + sticky: { type: 'boolean', required: false }, + type: { type: 'enum', values: this.config.modelValidate.comment.type.optional, required: false }, + article: { type: 'objectId', required: false }, + partner: { type: 'objectId', required: false }, + forward: { type: 'objectId', required: false }, + author: { type: 'object', required: true } + } } } @@ -19,7 +28,38 @@ module.exports = class CommentService extends ProxyService { async item () {} - async create () {} + async create () { + const { ctx } = this + const body = ctx.validateBody(this.rules.create) + const { article, parent, forward } = body + if (type === this.config.modelValidate.comment.type.optional.COMMENT) { + if (!article) { + return ctx.fail(422, '缺少文章ID') + } + } + if ((parent && !forward) || (!parent && forward)) { + return ctx.fail(422, '缺少parent和forward参数') + } + const user = await this.service.user.checkCommentAuthor(author) + if (!user) { + return ctx.fail('用户不存在') + } else if (user.mute) { + // 被禁言 + return ctx.fail('该用户已被禁言') + } + body.author = user._id + if (!this.service.user.checkUserSpam(user)) { + return ctx.fail('该用户的垃圾评论数量已达到最大限制,已被禁言') + } + const isAdmin = user.role === this.config.modelValidate.user.role.optional.ADMIN + const { ip, location } = ctx.getLocation() + body.meta = { + location, + ip, + ua: ctx.req.headers['user-agent'] || '', + referer: ctx.req.headers['referer'] || '' + } + } async update () {} diff --git a/app/service/user.js b/app/service/user.js index 3ec1a9b..9b07e09 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -37,4 +37,51 @@ module.exports = class UserService extends ProxyService { } return await this.findById(params.id).select(select).exec() } + + async checkCommentAuthor (author) { + let user = null + const { isObjectId, isObject } = this.app.utils.validate + if (isObjectId(author)) { + user = this.findById(author).select('-password').exec90 + } else if (isObject(author)) { + const update = {} + author.name && (update.name = author.name) + author.site && (update.site = author.site) + if (author.email) { + update.avatar = this.app.utils.gravatar(author.email) + update.email = author.email + } + if (author.id) { + // 更新 + if (isObjectId(author.id)) { + user = await this.updateById(author.id, update).exec() + if (user) { + this.logger.info(`用户【${user.name}】更新成功`) + } + } + } else { + // 创建 + user = await this.newAndSave(Object.assign(update, { + role: config.constant.roleMap.USER + })) + if (user) { + this.logger.info(`用户【${user.name}】创建成功`) + } + } + } + } + + // 检测用户以往spam评论 + async checkUserSpam (user) { + const comments = await this.service.comment.find({ author: user._id }).exec() + const spams = comments.filter(c => c.spam) + if (spams.length >= this.config.limit.commentSpamLimit) { + if (!user.mute) { + await this.updateById(user._id, { mute: true }).exec() + this.logger.info(`用户【${user.name}】禁言成功`) + } + return false + } + return true + } } diff --git a/app/utils/gravatar.js b/app/utils/gravatar.js new file mode 100644 index 0000000..9bc7152 --- /dev/null +++ b/app/utils/gravatar.js @@ -0,0 +1,21 @@ +/** + * @desc gravatar头像 + */ + +const gravatar = require('gravatar') + +module.exports = app => { + return (email = '', opt = {}) => { + if (!app.utils.validate.isEmail(email)) { + return app.config.defaultAvatar + } + const protocol = `http${app.config.isProd ? 's' : ''}` + const url = gravatar.url(email, Object.assign({ + s: '100', + r: 'x', + d: 'retro', + protocol + }, opt)) + return url.replace(`${protocol}://`, `${app.config.site}/proxy/`) + } +} diff --git a/config/config.default.js b/config/config.default.js index 8c9b459..42dff76 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -6,7 +6,7 @@ module.exports = appInfo => { config.version = appInfo.pkg.version - config.site = appInfo.pkg.author.site + config.site = appInfo.pkg.author.url config.isLocal = appInfo.env === 'local' @@ -43,6 +43,13 @@ module.exports = appInfo => { error: true } + config.akismet = { + client: { + blog: config.site, + key: '7fa12f4a1d08' + } + } + // mongoose配置 config.mongoose = { url: 'mongodb://127.0.0.1/node-server', @@ -128,14 +135,6 @@ module.exports = appInfo => { MESSAGE: '1', OTHER: '2' } - }, - // 是否置顶 0 否 | 1 是 - sticky: { - default: '0', - optional: { - NORMAL: '0', - STICKY: '1' - } } } } @@ -146,5 +145,14 @@ module.exports = appInfo => { password: 'admin123456' } + config.defaultAvatar = 'http://static.jooger.me/img/common/default-avatar.png' + + // 限制参数 + config.limit = { + relatedArticleLimit: 10, + commentSpamLimit: 3, + hotLimit: 7 + } + return config } diff --git a/config/plugin.js b/config/plugin.js index f244c73..a5727fe 100644 --- a/config/plugin.js +++ b/config/plugin.js @@ -1,5 +1,7 @@ 'use strict' +const path = require('path') + // had enabled by egg // exports.static = true @@ -22,3 +24,8 @@ exports.redis = { enable: true, package: 'egg-redis' } + +exports.akismet = { + enable: true, + path: path.join(__dirname, '../app/lib/plugin/egg-akismet') +} diff --git a/package.json b/package.json index bd2c994..cd772f6 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "", "private": true, "dependencies": { + "akismet-api": "^4.2.0", "axios": "^0.18.0", "bcryptjs": "^2.4.3", "egg": "^2.2.1", @@ -13,12 +14,14 @@ "egg-scripts": "^2.5.0", "egg-validate": "^1.1.1", "geoip-lite": "^1.3.2", + "gravatar": "^1.6.0", "highlight.js": "^9.12.0", "jsonwebtoken": "^8.3.0", "koa-compose": "^4.1.0", "koa-is-json": "^1.0.0", "lodash": "^4.17.10", "marked": "^0.5.0", + "mongoose": "5.2.8", "mongoose-paginate-v2": "^1.0.12", "validator": "^10.6.0", "zlib": "^1.0.5" From 046e036f2b2f5444c23bc32c0be73008afb0f52f Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 26 Aug 2018 20:16:00 +0800 Subject: [PATCH 119/208] update: comment controller done --- app.js | 1 + app/controller/auth.js | 7 +- app/controller/comment.js | 48 ++- app/controller/setting.js | 2 +- app/lib/plugin/egg-akismet/agent.js | 4 +- app/lib/plugin/egg-akismet/lib/akismet.js | 6 +- app/lib/plugin/egg-akismet/package.json | 1 - app/lib/plugin/egg-mailer/agent.js | 4 + app/lib/plugin/egg-mailer/app.js | 3 + .../egg-mailer/config/config.default.js | 5 + app/lib/plugin/egg-mailer/lib/mailer.js | 28 ++ app/lib/plugin/egg-mailer/package.json | 5 + app/middleware/auth.js | 2 +- app/model/comment.js | 22 +- app/model/setting.js | 5 +- app/router.js | 16 +- app/service/article.js | 32 +- app/service/auth.js | 18 +- app/service/comment.js | 398 +++++++++++++++++- app/service/common.js | 31 +- app/service/proxy.js | 11 + app/service/setting.js | 26 +- app/service/user.js | 33 +- app/utils/gravatar.js | 2 +- config/config.default.js | 11 +- config/plugin.js | 5 + package.json | 1 + 27 files changed, 667 insertions(+), 60 deletions(-) create mode 100644 app/lib/plugin/egg-mailer/agent.js create mode 100644 app/lib/plugin/egg-mailer/app.js create mode 100644 app/lib/plugin/egg-mailer/config/config.default.js create mode 100644 app/lib/plugin/egg-mailer/lib/mailer.js create mode 100644 app/lib/plugin/egg-mailer/package.json diff --git a/app.js b/app.js index 4769e20..39ccb93 100644 --- a/app.js +++ b/app.js @@ -8,6 +8,7 @@ module.exports = app => { setSessionstore(app) app.beforeStart(async () => { const ctx = app.createAnonymousContext() + await ctx.service.setting.seed() await ctx.service.auth.seed() }) } diff --git a/app/controller/auth.js b/app/controller/auth.js index 1732b69..170c14c 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -16,7 +16,12 @@ module.exports = class AuthController extends Controller { } async info () { - await this.service.auth.info() + const data = await this.service.auth.info() + if (data.info) { + ctx.success(data) + } else { + ctx.fail(401) + } } async update () { diff --git a/app/controller/comment.js b/app/controller/comment.js index be85dda..e91f024 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -5,15 +5,51 @@ const { Controller } = require('egg') module.exports = class CommentController extends Controller { - async list () {} + async list () { + const { ctx } = this + const data = await this.service.comment.list() + data + ? ctx.success(data, '评论列表获取成功') + : ctx.fail('评论列表获取失败') + } - async item () {} + async item () { + const { ctx } = this + const data = await this.service.comment.item() + data + ? ctx.success(data, '评论详情获取成功') + : ctx.fail('评论详情获取失败') + } - async create () {} + async create () { + const data = await this.service.comment.create() + if (data) { + if (data.type === this.config.modelValidate.comment.type.optional.COMMENT) { + // 如果是文章评论,则更新文章评论数量 + this.service.article.updateArticleCommentCount(data.article) + } + // 发送邮件通知站主和被评论者 + this.service.comment.sendCommentEmailToAdminAndUser(data) + } + } - async update () {} + async update () { + await this.service.comment.update() + } - async delete () {} + async delete () { + const { ctx } = this + const data = await this.service.comment.delete() + data + ? ctx.success('评论删除成功') + : ctx.fail('评论删除失败') + } - async like () {} + async like () { + const { ctx } = this + const data = await this.service.comment.update() + data + ? ctx.success(data, '评论点赞成功') + : ctx.fail('评论点赞失败') + } } diff --git a/app/controller/setting.js b/app/controller/setting.js index 50cdcf9..3d7d334 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -15,7 +15,7 @@ module.exports = class SettingController extends Controller { async update () { const { ctx } = this - const data = await this.service.setting.update() + const data = await this.service.setting.update(ctx.request.body) data ? ctx.success(data, '数据更新成功') : ctx.fail('数据更新失败') diff --git a/app/lib/plugin/egg-akismet/agent.js b/app/lib/plugin/egg-akismet/agent.js index b24b820..53e9ad7 100644 --- a/app/lib/plugin/egg-akismet/agent.js +++ b/app/lib/plugin/egg-akismet/agent.js @@ -1,3 +1,3 @@ module.exports = agent => { - if (agent.config.akismet.agent) require('./lib/akismet')(agent); -}; + if (agent.config.akismet.agent) require('./lib/akismet')(agent) +} diff --git a/app/lib/plugin/egg-akismet/lib/akismet.js b/app/lib/plugin/egg-akismet/lib/akismet.js index 9985531..f5f8f3f 100644 --- a/app/lib/plugin/egg-akismet/lib/akismet.js +++ b/app/lib/plugin/egg-akismet/lib/akismet.js @@ -67,7 +67,7 @@ class AkismetClient { this.app.coreLogger.warn('[egg-akismet] 评论验证不通过,疑似垃圾评论') resolve(true) } else { - this.app.coreLogger.success('[egg-akismet] 评论验证通过') + this.app.coreLogger.info('[egg-akismet] 评论验证通过') resolve(false) } }) @@ -88,7 +88,7 @@ class AkismetClient { this.app.coreLogger.error('[egg-akismet] 误检Spam垃圾评论报告提交失败') return reject(err) } - this.app.coreLogger.success('[egg-akismet] 误检Spam垃圾评论报告提交成功') + this.app.coreLogger.info('[egg-akismet] 误检Spam垃圾评论报告提交成功') resolve() }) } else { @@ -108,7 +108,7 @@ class AkismetClient { this.app.coreLogger.error('[egg-akismet] 误检正常评论报告提交失败') return reject(err) } - this.app.coreLogger.success('[egg-akismet] 误检正常评论报告提交成功') + this.app.coreLogger.info('[egg-akismet] 误检正常评论报告提交成功') resolve() }) } else { diff --git a/app/lib/plugin/egg-akismet/package.json b/app/lib/plugin/egg-akismet/package.json index 7ccbdbf..2b8a739 100644 --- a/app/lib/plugin/egg-akismet/package.json +++ b/app/lib/plugin/egg-akismet/package.json @@ -1,5 +1,4 @@ { - "name": "egg-akismet", "eggPlugin": { "name": "akismet" } diff --git a/app/lib/plugin/egg-mailer/agent.js b/app/lib/plugin/egg-mailer/agent.js new file mode 100644 index 0000000..12473a2 --- /dev/null +++ b/app/lib/plugin/egg-mailer/agent.js @@ -0,0 +1,4 @@ +module.exports = agent => { + if (agent.config.mailer.agent) require('./lib/mailer')(agent) +} + \ No newline at end of file diff --git a/app/lib/plugin/egg-mailer/app.js b/app/lib/plugin/egg-mailer/app.js new file mode 100644 index 0000000..b7a971c --- /dev/null +++ b/app/lib/plugin/egg-mailer/app.js @@ -0,0 +1,3 @@ +module.exports = app => { + if (app.config.mailer.app) require('./lib/mailer')(app) +} \ No newline at end of file diff --git a/app/lib/plugin/egg-mailer/config/config.default.js b/app/lib/plugin/egg-mailer/config/config.default.js new file mode 100644 index 0000000..de9e3b2 --- /dev/null +++ b/app/lib/plugin/egg-mailer/config/config.default.js @@ -0,0 +1,5 @@ +exports.mailer = { + app: true, + agent: false, + client: {} +} diff --git a/app/lib/plugin/egg-mailer/lib/mailer.js b/app/lib/plugin/egg-mailer/lib/mailer.js new file mode 100644 index 0000000..77707d9 --- /dev/null +++ b/app/lib/plugin/egg-mailer/lib/mailer.js @@ -0,0 +1,28 @@ +const nodemailer = require('nodemailer') + +module.exports = app => { + app.addSingleton('mailer', createClient) +} + +function createClient (config, app) { + return { + client: null, + getClient (opt) { + return this.client || (this.client = nodemailer.createTransport(Object.assign({}, config, opt))) + }, + async verify () { + await new Promise((resolve, reject) => { + if (!this.client) { + return resolve() + } + this.client.verify(err => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } + } +} diff --git a/app/lib/plugin/egg-mailer/package.json b/app/lib/plugin/egg-mailer/package.json new file mode 100644 index 0000000..24a61b9 --- /dev/null +++ b/app/lib/plugin/egg-mailer/package.json @@ -0,0 +1,5 @@ +{ + "eggPlugin": { + "name": "mailer" + } +} diff --git a/app/middleware/auth.js b/app/middleware/auth.js index 2a2558d..ced23a5 100644 --- a/app/middleware/auth.js +++ b/app/middleware/auth.js @@ -20,7 +20,7 @@ module.exports = app => { return ctx.fail(401, '用户不存在') } ctx._user = user.toObject() - ctx._isAuthenticated = true + ctx._isAuthed = true await next() } ]) diff --git a/app/model/comment.js b/app/model/comment.js index 6530cd1..1c77924 100644 --- a/app/model/comment.js +++ b/app/model/comment.js @@ -52,5 +52,25 @@ module.exports = app => { forward: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' } // 前一条评论ID,可以是parent的id, 比如 B评论 是 A评论的回复,则B.forward._id = A._id,主要是为了查看评论对话时的评论树构建 }) - return mongoose.model('Comment', app.processSchema(CommentSchema)) + return mongoose.model('Comment', app.processSchema(CommentSchema, { + paginate: true + }, { + pre: { + save (next) { + this.renderedContent = app.utils.markdown.render(this.content) + next() + }, + async findOneAndUpdate () { + delete this._update.updatedAt + const { content } = this._update + const find = await this.findOne() + if (find) { + if (content !== find.content) { + this._update.renderedContent = app.utils.markdown.render(content) + this._update.updatedAt = Date.now() + } + } + } + } + })) } diff --git a/app/model/setting.js b/app/model/setting.js index dcdbf2b..b5aec1a 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -39,8 +39,9 @@ module.exports = app => { region: { type: String, default: '' } }, // 163邮箱 - mail163: { - password: { type: String, default: '' } + mail: { + user: { type: String, default: '' }, + pass: { type: String, default: '' } }, // gayhub github: { diff --git a/app/router.js b/app/router.js index 2bada04..0eaf618 100644 --- a/app/router.js +++ b/app/router.js @@ -7,7 +7,7 @@ module.exports = app => { version: config.version, author: config.pkg.author, github: 'https://github.com/jo0ger', - site: config.site, + site: config.author.url, poweredBy: ['Egg', 'Koa2', 'MongoDB', 'Nginx', 'Redis'] } }) @@ -38,6 +38,12 @@ function frontend (app) { router.get('/tags', controller.tag.list) router.get('/tags/:id', controller.tag.item) + // Comment + router.get('/comments', controller.comment.list) + router.get('/comments/:id', controller.comment.item) + router.post('/comments', controller.comment.create) + router.post('/comments/:id/like', controller.comment.like) + // User router.get('/users/:id', controller.user.item) @@ -77,6 +83,14 @@ function backend (app) { router.patch('/backend/tags/:id', auth, controller.tag.update) router.delete('/backend/tags/:id', auth, controller.tag.delete) + // Comment + router.get('/backend/comments', auth, controller.comment.list) + router.get('/backend/comments/:id', auth, controller.comment.item) + router.post('/backend/comments', auth, controller.comment.create) + router.patch('/backend/comments/:id', auth, controller.comment.update) + router.delete('/backend/comments/:id', auth, controller.comment.delete) + router.post('/backend/comments/:id/like', auth, controller.comment.like) + // User router.get('/backend/users', auth, controller.user.list) router.get('/backend/users/:id', auth, controller.user.item) diff --git a/app/service/article.js b/app/service/article.js index 4416027..f2ad8ef 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -116,7 +116,7 @@ module.exports = class ArticleService extends ProxyService { } // 未通过权限校验(前台获取文章列表) - if (!ctx._isAuthenticated) { + if (!ctx._isAuthed) { // 将文章状态重置为1 query.state = 1 // 文章列表不需要content和state @@ -156,7 +156,7 @@ module.exports = class ArticleService extends ProxyService { ctx.validate(this.rules.item, ctx.query) let query = null // 只有前台博客访问文章的时候pv才+1 - if (!ctx._isAuthenticated) { + if (!ctx._isAuthed) { query = this.updateOne({ _id: params.id, state: this.config.modelValidate.article.state.optional.PUBLISH }, { $inc: { 'meta.pvs': 1 } }).select('-content') } else { query = this.findById(params.id) @@ -233,7 +233,7 @@ module.exports = class ArticleService extends ProxyService { title: 1, createdAt: 1 } - if (!this.ctx._isAuthenticated) { + if (!this.ctx._isAuthed) { $match.state = 1 } else { $project.state = 1 @@ -313,7 +313,7 @@ module.exports = class ArticleService extends ProxyService { if (!data || !data._id) return null const query = {} // 如果未通过权限校验,将文章状态重置为1 - if (!ctx._isAuthenticated) { + if (!ctx._isAuthed) { query.state = this.config.modelValidate.article.state.optional.PUBLISH } const prev = await this.service.article.findOne(query) @@ -347,4 +347,28 @@ module.exports = class ArticleService extends ProxyService { next: next ? next.toObject() : null } } + + async updateArticleCommentCount (articleIds = []) { + if (!this.app.utils.validate.isArray(articleIds)) { + articleIds = [articleIds] + } + if (!articleIds.length) return + const { validate, share } = this.app.utils + // TIP: 这里必须$in的是一个ObjectId对象数组,而不能只是id字符串数组 + articleIds = [...new Set(articleIds)].filter(id => validate.isObjectId(id)).map(id => share.createObjectId(id)) + const counts = await this.service.comment.aggregate([ + { $match: { state: 1, article: { $in: articleIds } } }, + { $group: { _id: '$article', total_count: { $sum: 1 } } } + ]) + .exec() + .catch(err => { + this.logger.error('更新文章评论数量前聚合评论数据操作失败,错误:' + err.message) + return [] + }) + Promise.all( + counts.map(count => articleProxy.updateById(count._id, { $set: { 'meta.comments': count.total_count } }).exec()) + ) + .then(() => this.logger.info('文章评论数量更新成功')) + .catch(err => this.logger.error('文章评论数量更新失败,错误:' + err.message)) + } } diff --git a/app/service/auth.js b/app/service/auth.js index fafe122..40792da 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -70,20 +70,16 @@ module.exports = class AuthService extends Service { async info () { const { ctx } = this const adminId = ctx._user._id - if (!adminId && !ctx._isAuthenticated) { + if (!adminId && !ctx._isAuthed) { return ctx.fail(401) } let data = null - if (ctx._isAuthenticated) { + if (ctx._isAuthed) { data = await this.service.user.findById(adminId).select('-password').exec() } - if (data) { - ctx.success({ - info: data, - token: ctx.session._token - }) - } else { - ctx.fail(401) + return { + info: data, + token: ctx.session._token } } @@ -120,7 +116,7 @@ module.exports = class AuthService extends Service { if (!admin) { return this.logger.warn('管理员创建失败') } - const data = await this.service.user.newAndSave({ + const data = await this.service.user.create({ role: ADMIN, name: admin.name, email: admin.email || this.config.pkg.author.email, @@ -135,7 +131,7 @@ module.exports = class AuthService extends Service { login: admin.login } }) - if (!data || !data.length) { + if (!data) { return this.logger.warn(`管理员【${admin.name}】创建失败`) } this.logger.info(`管理员【${admin.name}】创建成功`) diff --git a/app/service/comment.js b/app/service/comment.js index e4d9e14..f757d74 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -11,27 +11,217 @@ module.exports = class CommentService extends ProxyService { get rules () { return { + list: { + page: { type: 'number', required: true, min: 1 }, + limit: { type: 'number', required: true, min: 1 }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, + type: { type: 'enum', values: Object.values(this.config.modelValidate.comment.type.optional), required: false }, + author: { type: 'objectId', required: false }, + article: { type: 'objectId', required: false }, + parent: { type: 'objectId', required: false }, + keyword: { type: 'string', required: false }, + // 时间区间查询仅后台可用,且依赖于createdAt + startDate: { type: 'dateTime', required: false }, + endDate: { type: 'dateTime', required: false }, + // 排序仅后台能用,且order和sortBy需同时传入才起作用 + // -1 desc | 1 asc + order: { type: 'enum', values: [-1, 1], required: false }, + sortBy: { type: 'enum', values: ['createdAt', 'updatedAt', 'ups'], required: false } + }, create: { content: { type: 'string', required: true }, - state: { type: 'enum', values: this.config.modelValidate.comment.state.optional, required: false }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, sticky: { type: 'boolean', required: false }, - type: { type: 'enum', values: this.config.modelValidate.comment.type.optional, required: false }, + type: { type: 'enum', values: Object.values(this.config.modelValidate.comment.type.optional), required: false }, article: { type: 'objectId', required: false }, partner: { type: 'objectId', required: false }, forward: { type: 'objectId', required: false }, author: { type: 'object', required: true } + }, + update: { + content: { type: 'string', required: false }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, + sticky: { type: 'boolean', required: false } } } } - async list () {} + async list () { + const { ctx } = this + ctx.query.page = Number(ctx.query.page) + ctx.query.limit = Number(ctx.query.limit) + ctx.validate(this.rules.list, ctx.query) + const { page, limit, state, type, keyword, author, article, parent, order, sortBy, startDate, endDate } = ctx.query + // 过滤条件 + const options = { + sort: { createdAt: 1 }, + page, + limit, + select: '', + populate: [ + { + path: 'author', + select: !ctx._isAuthed ? 'github avatar name site' : '' + }, + { + path: 'parent', + select: 'author meta sticky ups', + match: { + state: 1 + } + }, + { + path: 'forward', + select: 'author meta sticky ups', + match: { + state: 1 + }, + populate: { + path: 'author', + select: 'avatar github name' + } + } + ] + } + + // 查询条件 + const query = {} + if (state !== undefined) { + query.state = state + } + + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { title: keywordReg } + ] + } + + // 用户 + if (author) { + // 如果是id + if (this.app.utils.validate.isObjectId(author)) { + query.author = author + } else { + // 普通字符串,需要先查到id + const u = await this.service.user.findOne({ name: author }).exec() + query.author = u ? u._id : this.app.utils.share.createObjectId() + } + } + + // 文章 + if (article) { + // 如果是id + if (this.app.utils.validate.isObjectId(article)) { + query.article = article + } else { + // 普通字符串,需要先查到id + const a = await this.service.article.findOne({ name: article }).exec() + query.article = a ? a._id : this.app.utils.share.createObjectId() + } + } + + if (parent) { + // 获取子评论 + query.parent = parent + } else { + // 获取父评论 + query.parent = { $exists: false } + } - async item () {} + // 未通过权限校验(前台获取文章列表) + if (!ctx._isAuthed) { + // 将评论状态重置为1 + query.state = 1 + query.spam = false + // 评论列表不需要content和state + options.select = '-content -state -updatedAt -spam -type' + } else { + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } + + // 起始日期 + if (startDate) { + const $gte = new Date(startDate) + if ($gte.toString() !== 'Invalid Date') { + query.createdAt = { $gte } + } + } + + // 结束日期 + if (endDate) { + const $lte = new Date(endDate) + if ($lte.toString() !== 'Invalid Date') { + query.createdAt = Object.assign({}, query.createdAt, { $lte }) + } + } + } + const comments = await this.service.comment.paginate(query, options) + if (!comments) return null + const data = [] + // 查询子评论数量 + await Promise.all(comments.docs.map(doc => { + doc = doc.toObject() + doc.subCount = 0 + data.push(doc) + return this.service.comment.count({ parent: doc._id }).exec() + .then(count => { + doc.subCount = count + }) + })) + comments.docs = data + return this.app.utils.share.getDocsPaginationData(comments) + } + + async item () { + const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + let data = null + let queryPs = null + if (!ctx._isAuthed) { + queryPs = this.findOne({ _id: params.id, state: 1, spam: false }) + .select('-content -state -updatedAt -type -spam') + .populate({ + path: 'author', + select: 'github' + }) + .populate({ + path: 'parent', + select: 'author meta sticky ups' + }) + .populate({ + path: 'forward', + select: 'author meta sticky ups' + }) + } else { + queryPs = this.findById(id) + } + + data = await queryPs.populate([ + { + path: 'author', + select: 'github' + }, { + path: 'parent', + select: 'author meta sticky ups' + }, { + path: 'forward', + select: 'author meta sticky ups' + } + ]).exec() + + return data + } async create () { const { ctx } = this const body = ctx.validateBody(this.rules.create) - const { article, parent, forward } = body + const { article, parent, forward, type, author, content } = body if (type === this.config.modelValidate.comment.type.optional.COMMENT) { if (!article) { return ctx.fail(422, '缺少文章ID') @@ -51,7 +241,6 @@ module.exports = class CommentService extends ProxyService { if (!this.service.user.checkUserSpam(user)) { return ctx.fail('该用户的垃圾评论数量已达到最大限制,已被禁言') } - const isAdmin = user.role === this.config.modelValidate.user.role.optional.ADMIN const { ip, location } = ctx.getLocation() body.meta = { location, @@ -59,14 +248,207 @@ module.exports = class CommentService extends ProxyService { ua: ctx.req.headers['user-agent'] || '', referer: ctx.req.headers['referer'] || '' } + // 永链 + const permalink = this.getPermalink(body) + const isSpam = await this.app.akismet.checkSpam({ + user_ip: ip, + user_agent: body.meta.ua, + referrer: body.meta.referer, + permalink, + comment_type: getCommentType(type), + comment_author: user.name, + comment_author_email: user.email, + comment_author_url: user.site, + comment_content: content, + is_test: this.app.config.isProd + }) + // 如果是Spam评论 + if (isSpam) { + return ctx.fail('检测为垃圾评论,该评论将不会显示') + } + body.renderedContent = this.app.utils.markdown.render(content) + let data = await this.newAndSave(body) + if (data && data.length) { + data = data[0] + if (!ctx._isAuthed) { + data = await this.findById(data._id) + .select('-content -state -updatedAt') + .populate({ + path: 'author', + select: 'name site avatar role mute email' + }) + .populate({ + path: 'parent', + select: 'author meta sticky ups' + }) + .populate({ + path: 'forward', + select: 'author meta sticky ups' + }) + } else { + data = await this.findById(data._id).exec() + } + ctx.success(data, '评论发布成功') + } else { + ctx.fail('评论发布失败') + } + return data } - async update () {} + async update () { + const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + const body = ctx.validateBody(this.rules.create) + let cache = await this.findById(params.id).populate('author') + if (!cache) { + return ctx.fail('评论不存在') + } + cache = cache.toObject() + if (ctx._isAuthed && ctx._user._id.toString() !== cache.author._id.toString()) { + return ctx.fail('其他人的评论内容不能修改') + } + + if (body.content !== undefined) { + body.renderedContent = this.app.utils.markdown.render(body.content) + } + + // 状态修改是涉及到spam修改 + if (body.state !== undefined) { + const permalink = this.getPermalink(cache) + const opt = { + user_ip: cache.meta.ip, + user_agent: cache.meta.ua, + referrer: cache.meta.referer, + permalink, + comment_type: getCommentType(cache.type), + comment_author: cache.author.github.login, + comment_author_email: cache.author.github.email, + comment_author_url: cache.author.github.blog, + comment_content: cache.content, + is_test: isProd + } + const SPAM = this.config.modelValidate.comment.state.optional.SPAM + if (cache.state === SPAM && state !== SPAM) { + // 垃圾评论转为正常评论 + if (cache.spam) { + body.spam = false + // 报告给Akismet + this.app.akismet.submitSpam(opt) + } + } else if (cache.state !== SPAM && state === SPAM) { + // 正常评论转为垃圾评论 + if (!cache.spam) { + body.spam = true + // 报告给Akismet + this.app.akismet.submitHam(opt) + } + } + } + let data = null + if (!ctx._isAuthed) { + data = await this.updateById(id, comment).select('-content -state -updatedAt') + .populate({ + path: 'author', + select: 'github' + }) + .populate({ + path: 'parent', + select: 'author meta sticky ups' + }) + .populate({ + path: 'forward', + select: 'author meta sticky ups' + }).exec() + } else { + data = await this.updateById(id, comment).exec() + } + data + ? ctx.success(data, '评论更新成功') + : ctx.fail('评论更新失败') + } async delete () { const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + const data = await this.deleteById(params.id).exec() + return data && data.ok && data.n + } + + async like () { + const { ctx } = this + const { params } = ctx ctx.validateParamsObjectId() + return await this.updateById(params.id, { + $inc: { + ups: 1 + } + }) } - async like () {} + async sendCommentEmailToAdminAndUser (comment) { + const { type, article } = comment + const commentType = this.config.modelValidate.comment.type.optional + const permalink = this.getPermalink(comment) + let adminTitle = '未知的评论' + let adminType = '评论' + if (type === commentType.COMMENT) { + // 文章评论 + const at = await this.service.article.findById(article).exec().catch(() => null) + if (at && at._id) { + adminTitle = `博客文章 [${at.title}] 有了新的评论` + } + adminType = '评论' + } else if (type === commentType.MESSAGE) { + // 站内留言 + adminTitle = `个人站点有新的留言` + adminType = '留言' + } + + // 发送给管理员邮箱config.email + this.service.common.sendMail({ + subject: adminTitle, + text: `来自 ${comment.author.name} 的${adminType}:${comment.content}`, + html: `

来自 ${comment.author.name} 的${adminType} [ 点击查看 ]:${comment.renderedContent}

` + }, true) + + // 发送给被评论者 + if (comment.forward && comment.forward._id !== comment.author._id) { + const forwardAuthor = await this.service.user.findById(comment.forward.author).exec().catch(() => null) + if (forwardAuthor) { + this.service.comment.sendMail({ + to: forwardAuthor.github.email, + subject: '你在 Jooger 的博客的评论有了新的回复', + text: `来自 ${comment.author.name} 的回复:${comment.content}`, + html: `

来自 ${comment.author.name} 的回复 [ 点击查看 ]:${comment.renderedContent}

` + }) + } + } + } + + getPermalink (comment = {}) { + const { type, article } = comment + const commentType = this.config.modelValidate.comment.type.optional + switch (type) { + case commentType.COMMENT: + return `${this.config.author.url}/articles/${article}` + case commentType.MESSAGE: + return `${this.config.author.url}/guestbook` + default: + return '' + } + } +} + +// 评论类型说明 +function getCommentType (type) { + switch (type) { + case 0: + return '文章评论' + case 1: + return '站点留言' + default: + return '评论' + } } diff --git a/app/service/common.js b/app/service/common.js index e869167..659f723 100644 --- a/app/service/common.js +++ b/app/service/common.js @@ -6,11 +6,12 @@ const { Service } = require('egg') const axios = require('axios') const prefix = 'http://' +let mailerClient = null module.exports = class UtilService extends Service { proxyUrl (url) { if (url.startsWith(prefix)) { - return url.replace(prefix, `${this.app.config.site}/proxy/`) + return url.replace(prefix, `${this.app.config.author.url}/proxy/`) } return url } @@ -87,4 +88,32 @@ module.exports = class UtilService extends Service { } return links } + + // 发送邮件 + async sendMail (data, toMe = false) { + let client = mailerClient + const keys = await this.service.setting.keys() + if (!client) { + mailerClient = client = this.app.mailer.getClient({ + auth: keys.mail + }) + await this.app.mailer.verify() + } + return new Promise((resolve, reject) => { + const opt = Object.assign({ + from: `${this.config.author.name} <${keys.mail.user}>` + }, data) + if (toMe) { + opt.to = keys.mail.user + } + client.sendMail(opt, (err, info) => { + if (err) { + this.logger.error('邮件发送失败,' + err.message) + return reject(err) + } + this.logger.info('邮件发送成功') + resolve(info) + }) + }) + } } diff --git a/app/service/proxy.js b/app/service/proxy.js index 895c993..4ffa884 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -5,6 +5,10 @@ const { Service } = require('egg') module.exports = class ProxyService extends Service { + init () { + return this.model.init() + } + newAndSave (docs) { if (!Array.isArray(docs)) { docs = [docs] @@ -63,4 +67,11 @@ module.exports = class ProxyService extends Service { count (query = {}) { return this.model.count(query) } + + res (data, error) { + return { + data, + error + } + } } diff --git a/app/service/setting.js b/app/service/setting.js index 55ac252..8c10921 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -17,11 +17,11 @@ module.exports = class SettingService extends ProxyService { create: { site: { type: 'object', - required: true + required: false }, keys: { type: 'object', - required: true + required: false } }, update: { @@ -45,17 +45,18 @@ module.exports = class SettingService extends ProxyService { if (filter) { query.select = filter.split(',').join(' ') } - if (!ctx._isAuthenticated) { + if (!ctx._isAuthed) { query.select = 'site' } return await this.findOne(query).exec() } async keys () { - return await this.findOne().select('keys').exec() + const data = await this.findOne().select('keys').exec() + return data && data.keys || {} } - async create (payload) { + async create () { const { ctx } = this const body = this.ctx.validateBody(this.rules.create, payload) const exist = await this.findOne().exec() @@ -65,12 +66,27 @@ module.exports = class SettingService extends ProxyService { return await this.newAndSave(body) } + async seed () { + const exist = await this.findOne().exec() + if (exist) { + return exist + } + const data = await this.newAndSave() + if (data && data.length) { + this.logger.info('Setting初始化成功') + } else { + this.logger.info('Setting初始化失败') + } + return data + } + async update (payload) { if (!payload) { // http request payload = await this.findOne().exec() if (!payload) return } + payload.site = payload.site || {} // 更新友链 payload.site.links = await this.service.common.generateLinks(payload.site.links) const data = await this.updateOne({}, payload).exec() diff --git a/app/service/user.js b/app/service/user.js index 9b07e09..792da4a 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -18,7 +18,7 @@ module.exports = class UserService extends ProxyService { async list () { const { ctx } = this let select = '-password' - if (!ctx._isAuthenticated) { + if (!ctx._isAuthed) { select += ' -createdAt -updatedAt -role' } return await this.find() @@ -32,17 +32,34 @@ module.exports = class UserService extends ProxyService { const { params } = ctx ctx.validateParamsObjectId() let select = '-password' - if (!ctx._isAuthenticated) { + if (!ctx._isAuthed) { select += ' -createdAt -updatedAt -github' } return await this.findById(params.id).select(select).exec() } + // 创建用户 + async create (user) { + const { name } = user + const exist = await this.findOne({ name }).exec() + if (exist) { + this.logger.warn(`用户已存在:${name}`) + return exist + } + const data = await this.newAndSave(user) + if (!data || !data.length) { + this.logger.error(`用户创建失败:${name}`) + return null + } + this.logger.error(`用户创建成功:${name}`) + return data[0] + } + async checkCommentAuthor (author) { let user = null const { isObjectId, isObject } = this.app.utils.validate if (isObjectId(author)) { - user = this.findById(author).select('-password').exec90 + user = this.findById(author).select('-password').exec() } else if (isObject(author)) { const update = {} author.name && (update.name = author.name) @@ -56,19 +73,17 @@ module.exports = class UserService extends ProxyService { if (isObjectId(author.id)) { user = await this.updateById(author.id, update).exec() if (user) { - this.logger.info(`用户【${user.name}】更新成功`) + this.logger.info('用户更新成功:' + user.name) } } } else { // 创建 - user = await this.newAndSave(Object.assign(update, { - role: config.constant.roleMap.USER + user = await this.create(Object.assign(update, { + role: this.config.modelValidate.user.role.optional.NORMAL })) - if (user) { - this.logger.info(`用户【${user.name}】创建成功`) - } } } + return user } // 检测用户以往spam评论 diff --git a/app/utils/gravatar.js b/app/utils/gravatar.js index 9bc7152..53c1e21 100644 --- a/app/utils/gravatar.js +++ b/app/utils/gravatar.js @@ -16,6 +16,6 @@ module.exports = app => { d: 'retro', protocol }, opt)) - return url.replace(`${protocol}://`, `${app.config.site}/proxy/`) + return url.replace(`${protocol}://`, `${app.config.author.url}/proxy/`) } } diff --git a/config/config.default.js b/config/config.default.js index 42dff76..08161af 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -6,7 +6,7 @@ module.exports = appInfo => { config.version = appInfo.pkg.version - config.site = appInfo.pkg.author.url + config.author = appInfo.pkg.author config.isLocal = appInfo.env === 'local' @@ -45,11 +45,18 @@ module.exports = appInfo => { config.akismet = { client: { - blog: config.site, + blog: config.author.url, key: '7fa12f4a1d08' } } + config.mailer = { + client: { + service: '163', + secure: true + } + } + // mongoose配置 config.mongoose = { url: 'mongodb://127.0.0.1/node-server', diff --git a/config/plugin.js b/config/plugin.js index a5727fe..d3bc506 100644 --- a/config/plugin.js +++ b/config/plugin.js @@ -29,3 +29,8 @@ exports.akismet = { enable: true, path: path.join(__dirname, '../app/lib/plugin/egg-akismet') } + +exports.mailer = { + enable: true, + path: path.join(__dirname, '../app/lib/plugin/egg-mailer') +} diff --git a/package.json b/package.json index cd772f6..2674c08 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "marked": "^0.5.0", "mongoose": "5.2.8", "mongoose-paginate-v2": "^1.0.12", + "nodemailer": "^4.6.8", "validator": "^10.6.0", "zlib": "^1.0.5" }, From 9cbd5610a5d578e01b27ba0091454304eb324850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 27 Aug 2018 21:00:11 +0800 Subject: [PATCH 120/208] refactor: category service and controller refactor, and add category service test case --- app/controller/auth.js | 1 + app/controller/category.js | 70 +++++++- app/extend/application.js | 4 + app/extend/context.js | 3 +- app/lib/plugin/egg-akismet/agent.js | 2 +- app/lib/plugin/egg-akismet/app.js | 2 +- app/lib/plugin/egg-akismet/lib/akismet.js | 190 +++++++++++----------- app/lib/plugin/egg-mailer/agent.js | 1 - app/lib/plugin/egg-mailer/app.js | 2 +- app/middleware/auth.js | 6 +- app/model/category.js | 2 - app/router.js | 90 +--------- app/router/backend.js | 53 ++++++ app/router/frontend.js | 30 ++++ app/service/article.js | 32 +++- app/service/auth.js | 4 +- app/service/category.js | 134 +++------------ app/service/categorybak.js | 128 +++++++++++++++ app/service/comment.js | 57 +++---- app/service/proxy.js | 19 +-- app/service/proxy2.js | 42 +++++ app/utils/share.js | 2 + config/plugin.js | 5 + package.json | 1 + test/app/controller/home.test.js | 21 --- test/app/service/category.test.js | 66 ++++++++ 26 files changed, 591 insertions(+), 376 deletions(-) create mode 100644 app/router/backend.js create mode 100644 app/router/frontend.js create mode 100644 app/service/categorybak.js create mode 100644 app/service/proxy2.js delete mode 100644 test/app/controller/home.test.js create mode 100644 test/app/service/category.test.js diff --git a/app/controller/auth.js b/app/controller/auth.js index 170c14c..124f2a1 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -16,6 +16,7 @@ module.exports = class AuthController extends Controller { } async info () { + const { ctx } = this const data = await this.service.auth.info() if (data.info) { ctx.success(data) diff --git a/app/controller/category.js b/app/controller/category.js index 9264b64..03479b5 100644 --- a/app/controller/category.js +++ b/app/controller/category.js @@ -5,9 +5,53 @@ const { Controller } = require('egg') module.exports = class CategoryController extends Controller { + get rules () { + return { + list: { + // 查询关键词 + keyword: { type: 'string', required: false } + }, + create: { + name: { type: 'string', required: true }, + description: { type: 'string', required: false }, + extends: { + type: 'array', + required: false, + itemType: 'object', + rule: { + key: 'string', + value: 'string' + } + } + }, + update: { + name: { type: 'string', required: false }, + description: { type: 'string', required: false }, + extends: { + type: 'array', + required: false, + itemType: 'object', + rule: { + key: 'string', + value: 'string' + } + } + } + } + } + async list () { const { ctx } = this - const data = await this.service.category.list() + ctx.validate(this.rules.list, ctx.query) + const query = {} + const { keyword } = ctx.query + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { name: keywordReg } + ] + } + const data = await this.service.category.getListByQuery(query) data ? ctx.success(data, '分类列表获取成功') : ctx.fail('分类列表获取失败') @@ -15,7 +59,8 @@ module.exports = class CategoryController extends Controller { async item () { const { ctx } = this - const data = await this.service.category.item() + const params = ctx.validateParamsObjectId() + const data = await this.service.category.getItemById(params.id) data ? ctx.success(data, '分类详情获取成功') : ctx.fail('分类详情获取失败') @@ -23,15 +68,23 @@ module.exports = class CategoryController extends Controller { async create () { const { ctx } = this - const data = await this.service.category.create() - data && data.length + const body = ctx.validateBody(this.service.category.rules.create) + const { name } = body + const exists = await this.service.category.getItem({ name }) + if (exists) { + return ctx.fail('分类已存在') + } + const data = await this.service.category.create(body) + data ? ctx.success(data[0], '分类创建成功') : ctx.fail('分类创建失败') } async update () { const { ctx } = this - const data = await this.service.category.update() + const params = ctx.validateParamsObjectId() + const body = ctx.validateBody(this.service.category.rules.update) + const data = await this.service.category.updateById(params.id, body) data ? ctx.success(data, '分类更新成功') : ctx.fail('分类更新失败') @@ -39,7 +92,12 @@ module.exports = class CategoryController extends Controller { async delete () { const { ctx } = this - const data = await this.service.category.delete() + const params = ctx.validateParamsObjectId() + const articles = await this.service.article.getListByQuery({ category: params._id }, 'title') + if (articles.length) { + return ctx.fail('该分类下还有文章,不能删除', articles) + } + const data = await this.service.category.deleteById(params.id) data ? ctx.success('分类删除成功') : ctx.fail('分类删除失败') diff --git a/app/extend/application.js b/app/extend/application.js index d29d751..5915188 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -1,4 +1,5 @@ const mongoosePaginate = require('mongoose-paginate-v2') +const lodash = require('lodash') module.exports = { // model schema处理 @@ -23,5 +24,8 @@ module.exports = { }) }) return schema + }, + merge () { + return lodash.merge.apply(null, Array.prototype.slice.call(arguments)) } } diff --git a/app/extend/context.js b/app/extend/context.js index 3967eaa..0a41648 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -2,7 +2,8 @@ const geoip = require('geoip-lite') module.exports = { validateParams (rules) { - return this.validate(rules, this.params) + this.validate(rules, this.params) + return this.params }, validateBody (rules, body) { body = body || this.request.body diff --git a/app/lib/plugin/egg-akismet/agent.js b/app/lib/plugin/egg-akismet/agent.js index 53e9ad7..eb07404 100644 --- a/app/lib/plugin/egg-akismet/agent.js +++ b/app/lib/plugin/egg-akismet/agent.js @@ -1,3 +1,3 @@ module.exports = agent => { - if (agent.config.akismet.agent) require('./lib/akismet')(agent) + if (agent.config.akismet.agent) require('./lib/akismet')(agent) } diff --git a/app/lib/plugin/egg-akismet/app.js b/app/lib/plugin/egg-akismet/app.js index 2f57c8f..190910e 100644 --- a/app/lib/plugin/egg-akismet/app.js +++ b/app/lib/plugin/egg-akismet/app.js @@ -1,3 +1,3 @@ module.exports = app => { if (app.config.akismet.app) require('./lib/akismet')(app) -} \ No newline at end of file +} diff --git a/app/lib/plugin/egg-akismet/lib/akismet.js b/app/lib/plugin/egg-akismet/lib/akismet.js index f5f8f3f..7f0d813 100644 --- a/app/lib/plugin/egg-akismet/lib/akismet.js +++ b/app/lib/plugin/egg-akismet/lib/akismet.js @@ -1,19 +1,19 @@ const akismet = require('akismet-api') module.exports = app => { - app.addSingleton('akismet', createClient) - app.beforeStart(async () => { - const { valid, error } = await app.akismet.verifyKey() - if (valid) { + app.addSingleton('akismet', createClient) + app.beforeStart(async () => { + const { valid, error } = await app.akismet.verifyKey() + if (valid) { app.coreLogger.info('[egg-akismet] 服务启动成功') - } else { - app.coreLogger.error(`[egg-akismet] 服务启动失败:${error}`) - } - }) + } else { + app.coreLogger.error(`[egg-akismet] 服务启动失败:${error}`) + } + }) } function createClient (config, app) { - return new AkismetClient(config, app) + return new AkismetClient(config, app) } // Akismet apikey是否验证通过 @@ -25,96 +25,96 @@ let isValidKey = false * @param {String} [required] blog Akismet blog */ class AkismetClient { - constructor (config, app) { - this.config = config + constructor (config, app) { + this.config = config this.app = app - this.initClient() - } + this.initClient() + } - initClient () { - this.client = akismet.client(this.config) - } + initClient () { + this.client = akismet.client(this.config) + } - async verifyKey () { - let valid = true - let error = '' - if (!isValidKey) { - const v = await this.client.verifyKey().catch(err => { - error = 'Apikey验证失败,错误:' + err.message - }) - valid = v - if (v) { - isValidKey = true - } else { - error = '无效的Apikey' - this.client = null - } - } - return { valid, error } - } + async verifyKey () { + let valid = true + let error = '' + if (!isValidKey) { + const v = await this.client.verifyKey().catch(err => { + error = 'Apikey验证失败,错误:' + err.message + }) + valid = v + if (v) { + isValidKey = true + } else { + error = '无效的Apikey' + this.client = null + } + } + return { valid, error } + } - // 检测是否是spam - checkSpam (opt = {}) { - this.app.coreLogger.info('验证评论中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.checkSpam(opt, (err, spam) => { - if (err) { - this.app.coreLogger.error('[egg-akismet] 评论验证失败,将跳过Spam验证,错误:', err.message) - return reject(false) - } - if (spam) { - this.app.coreLogger.warn('[egg-akismet] 评论验证不通过,疑似垃圾评论') - resolve(true) - } else { - this.app.coreLogger.info('[egg-akismet] 评论验证通过') - resolve(false) - } - }) - } else { - this.app.coreLogger.warn('[egg-akismet] Apikey未认证,将跳过Spam验证') - resolve(false) - } - }) - } + // 检测是否是spam + checkSpam (opt = {}) { + this.app.coreLogger.info('验证评论中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.checkSpam(opt, (err, spam) => { + if (err) { + this.app.coreLogger.error('[egg-akismet] 评论验证失败,将跳过Spam验证,错误:', err.message) + return reject(false) + } + if (spam) { + this.app.coreLogger.warn('[egg-akismet] 评论验证不通过,疑似垃圾评论') + resolve(true) + } else { + this.app.coreLogger.info('[egg-akismet] 评论验证通过') + resolve(false) + } + }) + } else { + this.app.coreLogger.warn('[egg-akismet] Apikey未认证,将跳过Spam验证') + resolve(false) + } + }) + } - // 提交被误检为spam的正常评论 - submitSpam (opt = {}) { - this.app.coreLogger.info('[egg-akismet] 误检Spam垃圾评论报告提交中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.submitSpam(opt, err => { - if (err) { - this.app.coreLogger.error('[egg-akismet] 误检Spam垃圾评论报告提交失败') - return reject(err) - } - this.app.coreLogger.info('[egg-akismet] 误检Spam垃圾评论报告提交成功') - resolve() - }) - } else { - this.app.coreLogger.warn('[egg-akismet] Apikey未认证,误检Spam垃圾评论报告提交失败') - resolve() - } - }) - } + // 提交被误检为spam的正常评论 + submitSpam (opt = {}) { + this.app.coreLogger.info('[egg-akismet] 误检Spam垃圾评论报告提交中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.submitSpam(opt, err => { + if (err) { + this.app.coreLogger.error('[egg-akismet] 误检Spam垃圾评论报告提交失败') + return reject(err) + } + this.app.coreLogger.info('[egg-akismet] 误检Spam垃圾评论报告提交成功') + resolve() + }) + } else { + this.app.coreLogger.warn('[egg-akismet] Apikey未认证,误检Spam垃圾评论报告提交失败') + resolve() + } + }) + } - // 提交被误检为正常评论的spam - submitHam (opt = {}) { - this.app.coreLogger.info('[egg-akismet] 误检正常评论报告提交中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.submitSpam(opt, err => { - if (err) { - this.app.coreLogger.error('[egg-akismet] 误检正常评论报告提交失败') - return reject(err) - } - this.app.coreLogger.info('[egg-akismet] 误检正常评论报告提交成功') - resolve() - }) - } else { - this.app.coreLogger.warn('[egg-akismet] Apikey未认证,误检正常评论报告提交失败') - resolve() - } - }) - } + // 提交被误检为正常评论的spam + submitHam (opt = {}) { + this.app.coreLogger.info('[egg-akismet] 误检正常评论报告提交中...') + return new Promise((resolve, reject) => { + if (isValidKey) { + this.client.submitSpam(opt, err => { + if (err) { + this.app.coreLogger.error('[egg-akismet] 误检正常评论报告提交失败') + return reject(err) + } + this.app.coreLogger.info('[egg-akismet] 误检正常评论报告提交成功') + resolve() + }) + } else { + this.app.coreLogger.warn('[egg-akismet] Apikey未认证,误检正常评论报告提交失败') + resolve() + } + }) + } } diff --git a/app/lib/plugin/egg-mailer/agent.js b/app/lib/plugin/egg-mailer/agent.js index 12473a2..ee55a47 100644 --- a/app/lib/plugin/egg-mailer/agent.js +++ b/app/lib/plugin/egg-mailer/agent.js @@ -1,4 +1,3 @@ module.exports = agent => { if (agent.config.mailer.agent) require('./lib/mailer')(agent) } - \ No newline at end of file diff --git a/app/lib/plugin/egg-mailer/app.js b/app/lib/plugin/egg-mailer/app.js index b7a971c..98e1a1b 100644 --- a/app/lib/plugin/egg-mailer/app.js +++ b/app/lib/plugin/egg-mailer/app.js @@ -1,3 +1,3 @@ module.exports = app => { if (app.config.mailer.app) require('./lib/mailer')(app) -} \ No newline at end of file +} diff --git a/app/middleware/auth.js b/app/middleware/auth.js index ced23a5..15b53aa 100644 --- a/app/middleware/auth.js +++ b/app/middleware/auth.js @@ -20,6 +20,7 @@ module.exports = app => { return ctx.fail(401, '用户不存在') } ctx._user = user.toObject() + ctx._isAdmin = user.role === app.config.modelValidate.user.role.optional.ADMIN ctx._isAuthed = true await next() } @@ -28,10 +29,7 @@ module.exports = app => { // 验证登录token function verifyToken (app) { - const { - config, - logger - } = app + const { config, logger } = app return async (ctx, next) => { ctx.session._verify = false const token = ctx.cookies.get(config.session.key) diff --git a/app/model/category.js b/app/model/category.js index 6dda587..570d631 100644 --- a/app/model/category.js +++ b/app/model/category.js @@ -15,8 +15,6 @@ module.exports = app => { createdAt: { type: Date, default: Date.now }, // 更新日期 updatedAt: { type: Date, default: Date.now }, - // 排序 首页分类展示顺序 - list: { type: Number, default: 1 }, // 扩展属性 extends: [{ key: { type: String, validate: /\S+/ }, diff --git a/app/router.js b/app/router.js index 0eaf618..efcbba6 100644 --- a/app/router.js +++ b/app/router.js @@ -12,8 +12,8 @@ module.exports = app => { } }) - frontend(app) - backend(app) + require('./router/backend')(app) + require('./router/frontend')(app) router.all('*', ctx => { const code = 404 @@ -21,89 +21,3 @@ module.exports = app => { }) } -function frontend (app) { - const { router, controller } = app - - // Article - router.get('/articles', controller.article.list) - router.get('/articles/archives', controller.article.archives) - router.get('/articles/:id', controller.article.item) - router.patch('/articles/:id', controller.article.like) - - // Category - router.get('/categories', controller.category.list) - router.get('/categories/:id', controller.category.item) - - // Tag - router.get('/tags', controller.tag.list) - router.get('/tags/:id', controller.tag.item) - - // Comment - router.get('/comments', controller.comment.list) - router.get('/comments/:id', controller.comment.item) - router.post('/comments', controller.comment.create) - router.post('/comments/:id/like', controller.comment.like) - - // User - router.get('/users/:id', controller.user.item) - - // Setting - router.get('/setting', controller.setting.index) - - return router -} - -function backend (app) { - const { router, controller, middlewares } = app - const auth = middlewares.auth(app) - - // Article - router.get('/backend/articles', auth, controller.article.list) - router.get('/backend/articles/archives', auth, controller.article.archives) - router.get('/backend/articles/:id', auth, controller.article.item) - router.post('/backend/articles', auth, controller.article.create) - router.put('/backend/articles/:id', auth, controller.article.update) - router.patch('/backend/articles/:id', auth, controller.article.update) - router.patch('/backend/articles/:id/like', auth, controller.article.like) - router.delete('/backend/articles/:id', auth, controller.article.delete) - - // Category - router.get('/backend/categories', auth, controller.category.list) - router.get('/backend/categories/:id', auth, controller.category.item) - router.post('/backend/categories', auth, controller.category.create) - router.put('/backend/categories/:id', auth, controller.category.update) - router.patch('/backend/categories/:id', auth, controller.category.update) - router.delete('/backend/categories/:id', auth, controller.category.delete) - - // Tag - router.get('/backend/tags', auth, controller.tag.list) - router.get('/backend/tags/:id', auth, controller.tag.item) - router.post('/backend/tags', auth, controller.tag.create) - router.put('/backend/tags/:id', auth, controller.tag.update) - router.patch('/backend/tags/:id', auth, controller.tag.update) - router.delete('/backend/tags/:id', auth, controller.tag.delete) - - // Comment - router.get('/backend/comments', auth, controller.comment.list) - router.get('/backend/comments/:id', auth, controller.comment.item) - router.post('/backend/comments', auth, controller.comment.create) - router.patch('/backend/comments/:id', auth, controller.comment.update) - router.delete('/backend/comments/:id', auth, controller.comment.delete) - router.post('/backend/comments/:id/like', auth, controller.comment.like) - - // User - router.get('/backend/users', auth, controller.user.list) - router.get('/backend/users/:id', auth, controller.user.item) - - // Setting - router.get('/backend/setting', auth, controller.setting.index) - router.put('/backend/setting', auth, controller.setting.update) - router.patch('/backend/setting', auth, controller.setting.update) - - // Auth - router.post('/backend/auth/login', controller.auth.login) - router.get('/backend/auth/logout', auth, controller.auth.logout) - router.get('/backend/auth/info', auth, controller.auth.info) - - return router -} diff --git a/app/router/backend.js b/app/router/backend.js new file mode 100644 index 0000000..a499089 --- /dev/null +++ b/app/router/backend.js @@ -0,0 +1,53 @@ +module.exports = app => { + const backendRouter = app.router.namespace('/v2/backend') + const { controller, middlewares } = app + const auth = middlewares.auth(app) + + // Article + backendRouter.get('/articles', auth, controller.article.list) + backendRouter.get('/articles/archives', auth, controller.article.archives) + backendRouter.get('/articles/:id', auth, controller.article.item) + backendRouter.post('/articles', auth, controller.article.create) + backendRouter.put('/articles/:id', auth, controller.article.update) + backendRouter.patch('/articles/:id', auth, controller.article.update) + backendRouter.patch('/articles/:id/like', auth, controller.article.like) + backendRouter.delete('/articles/:id', auth, controller.article.delete) + + // Category + backendRouter.get('/categories', auth, controller.category.list) + backendRouter.get('/categories/:id', auth, controller.category.item) + backendRouter.post('/categories', auth, controller.category.create) + backendRouter.put('/categories/:id', auth, controller.category.update) + backendRouter.patch('/categories/:id', auth, controller.category.update) + backendRouter.delete('/categories/:id', auth, controller.category.delete) + + // Tag + backendRouter.get('/tags', auth, controller.tag.list) + backendRouter.get('/tags/:id', auth, controller.tag.item) + backendRouter.post('/tags', auth, controller.tag.create) + backendRouter.put('/tags/:id', auth, controller.tag.update) + backendRouter.patch('/tags/:id', auth, controller.tag.update) + backendRouter.delete('/tags/:id', auth, controller.tag.delete) + + // Comment + backendRouter.get('/comments', auth, controller.comment.list) + backendRouter.get('/comments/:id', auth, controller.comment.item) + backendRouter.post('/comments', auth, controller.comment.create) + backendRouter.patch('/comments/:id', auth, controller.comment.update) + backendRouter.delete('/comments/:id', auth, controller.comment.delete) + backendRouter.post('/comments/:id/like', auth, controller.comment.like) + + // User + backendRouter.get('/users', auth, controller.user.list) + backendRouter.get('/users/:id', auth, controller.user.item) + + // Setting + backendRouter.get('/setting', auth, controller.setting.index) + backendRouter.put('/setting', auth, controller.setting.update) + backendRouter.patch('/setting', auth, controller.setting.update) + + // Auth + backendRouter.post('/auth/login', controller.auth.login) + backendRouter.get('/auth/logout', auth, controller.auth.logout) + backendRouter.get('/auth/info', auth, controller.auth.info) +} diff --git a/app/router/frontend.js b/app/router/frontend.js new file mode 100644 index 0000000..323448b --- /dev/null +++ b/app/router/frontend.js @@ -0,0 +1,30 @@ +module.exports = app => { + const fontendRouter = app.router.namespace('/v2') + const { router, controller } = app + + // Article + fontendRouter.get('/articles', controller.article.list) + fontendRouter.get('/articles/archives', controller.article.archives) + fontendRouter.get('/articles/:id', controller.article.item) + fontendRouter.patch('/articles/:id', controller.article.like) + + // Category + fontendRouter.get('/categories', controller.category.list) + fontendRouter.get('/categories/:id', controller.category.item) + + // Tag + fontendRouter.get('/tags', controller.tag.list) + fontendRouter.get('/tags/:id', controller.tag.item) + + // Comment + fontendRouter.get('/comments', controller.comment.list) + fontendRouter.get('/comments/:id', controller.comment.item) + fontendRouter.post('/comments', controller.comment.create) + fontendRouter.post('/comments/:id/like', controller.comment.like) + + // User + fontendRouter.get('/users/:id', controller.user.item) + + // Setting + fontendRouter.get('/setting', controller.setting.index) +} diff --git a/app/service/article.js b/app/service/article.js index f2ad8ef..5cd045f 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -53,6 +53,34 @@ module.exports = class ArticleService extends ProxyService { } } + getListByQuery (query, select = null, opt) { + return this.model.find(query, select, opt).exec() + } + + async getLimitListByQuery (query, opt) { + opt = this.app.merge({ + sort: { + updatedAt: -1, + createdAt: -1 + }, + page: 1, + limit: 10, + lean: true, + select: '-content -renderedContent', + populate: [ + { + path: 'category', + select: 'name description extends' + }, { + path: 'tag', + select: 'name description' + } + ] + }, opt) + const data = await this.model.paginate(query, opt) + return this.app.utils.share.getDocsPaginationData(data) + } + async list () { const { ctx } = this ctx.query.page = Number(ctx.query.page) @@ -145,7 +173,7 @@ module.exports = class ArticleService extends ProxyService { } } - const data = await this.service.article.paginate(query, options) + const data = await this.paginate(query, options) return this.app.utils.share.getDocsPaginationData(data) } @@ -290,7 +318,7 @@ module.exports = class ArticleService extends ProxyService { async getRelatedArticles (data) { if (!data || !data._id) return null const { _id, tag = [] } = data - const articles = await this.service.article.find({ + const articles = await this.article.find({ _id: { $nin: [ _id ] }, state: 1, tag: { $in: tag.map(t => t._id) } diff --git a/app/service/auth.js b/app/service/auth.js index 40792da..3115b06 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -132,8 +132,8 @@ module.exports = class AuthService extends Service { } }) if (!data) { - return this.logger.warn(`管理员【${admin.name}】创建失败`) + return this.logger.warn('管理员创建失败:' + admin.name) } - this.logger.info(`管理员【${admin.name}】创建成功`) + this.logger.info('管理员创建成功:' + admin.name) } } diff --git a/app/service/category.js b/app/service/category.js index f652f34..08341f0 100644 --- a/app/service/category.js +++ b/app/service/category.js @@ -2,127 +2,41 @@ * @desc 分类 Services */ -const ProxyService = require('./proxy') +const ProxyService = require('./proxy2') module.exports = class CategoryService extends ProxyService { get model () { return this.app.model.Category } - get rules () { - return { - list: { - // 查询关键词 - keyword: { type: 'string', required: false } - }, - create: { - name: { type: 'string', required: true }, - description: { type: 'string', required: false }, - list: { type: 'number', required: true }, - extends: { - type: 'array', - required: false, - itemType: 'object', - rule: { - key: 'string', - value: 'string' - } - } - }, - update: { - name: { type: 'string', required: false }, - description: { type: 'string', required: false }, - list: { type: 'number', required: true }, - extends: { - type: 'array', - required: false, - itemType: 'object', - rule: { - key: 'string', - value: 'string' - } - } - } - } - } - - async list () { - const { ctx, app, service } = this - ctx.validate(this.rules.list, ctx.query) - const query = {} - const { keyword } = ctx.query - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { name: keywordReg } - ] - } - const data = await this.find(query).sort('-createdAt').exec() - - if (data) { - const isFunction = app.utils.validate.isFunction - const PUBLISH = app.config.modelValidate.article.state.optional.PUBLISH - await Promise.all(data.map((item, index) => { - const toObject = item.toObject - if (isFunction(toObject)) { - item = item.toObject() - } - return service.article.find({ - category: item._id, - state: PUBLISH - }).exec().then(articles => { + async getListByQuery (query, select = null, opt) { + opt = this.app.merge({ + sort: '-createdAt' + }, opt) + const categories = await this.model.find(query, select, opt).exec() + if (categories.length) { + const PUBLISH = this.app.config.modelValidate.article.state.optional.PUBLISH + await Promise.all( + categories.map(async item => { + const articles = await this.service.article.getListByQuery({ + category: item._id, + state: PUBLISH + }) item.count = articles.length - data[index] = item }) - })) - } - - return data - } - - async item () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - let data = await this.findById(params.id).exec() - if (data) { - data = data.toObject() - const articles = await this.service.article.find({ category: params.id }) - .select('-category') - .exec() - data.articles = articles - data.articlesCount = articles.length + ) } - return data - } - - async create () { - const { ctx } = this - const body = this.ctx.validateBody(this.rules.create) - const exists = await this.find({ name: body.name }).exec() - if (exists && exists.length) { - ctx.throw(200, '分类已经存在') - } - return await this.newAndSave(body) - } - - async update () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - const body = this.ctx.validateBody(this.rules.update) - return await this.updateById(params.id, body).exec() + return categories } - async delete () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - const articles = await this.service.article.find({ category: params.id }).exec() - if (articles && articles.length) { - ctx.throw(200, '该分类下有文章,不能删除') + async getItem (query, select = null, opt) { + opt = this.app.merge({ + lean: true + }, opt) + const category = await this.model.findOne(query, select, opt).exec() + if (category) { + category.articles = await this.service.article.getListByQuery({ category: category._id }) } - const data = await this.deleteById(params.id).exec() - return data && data.ok && data.n + return category } } diff --git a/app/service/categorybak.js b/app/service/categorybak.js new file mode 100644 index 0000000..f652f34 --- /dev/null +++ b/app/service/categorybak.js @@ -0,0 +1,128 @@ +/** + * @desc 分类 Services + */ + +const ProxyService = require('./proxy') + +module.exports = class CategoryService extends ProxyService { + get model () { + return this.app.model.Category + } + + get rules () { + return { + list: { + // 查询关键词 + keyword: { type: 'string', required: false } + }, + create: { + name: { type: 'string', required: true }, + description: { type: 'string', required: false }, + list: { type: 'number', required: true }, + extends: { + type: 'array', + required: false, + itemType: 'object', + rule: { + key: 'string', + value: 'string' + } + } + }, + update: { + name: { type: 'string', required: false }, + description: { type: 'string', required: false }, + list: { type: 'number', required: true }, + extends: { + type: 'array', + required: false, + itemType: 'object', + rule: { + key: 'string', + value: 'string' + } + } + } + } + } + + async list () { + const { ctx, app, service } = this + ctx.validate(this.rules.list, ctx.query) + const query = {} + const { keyword } = ctx.query + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { name: keywordReg } + ] + } + const data = await this.find(query).sort('-createdAt').exec() + + if (data) { + const isFunction = app.utils.validate.isFunction + const PUBLISH = app.config.modelValidate.article.state.optional.PUBLISH + await Promise.all(data.map((item, index) => { + const toObject = item.toObject + if (isFunction(toObject)) { + item = item.toObject() + } + return service.article.find({ + category: item._id, + state: PUBLISH + }).exec().then(articles => { + item.count = articles.length + data[index] = item + }) + })) + } + + return data + } + + async item () { + const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + let data = await this.findById(params.id).exec() + if (data) { + data = data.toObject() + const articles = await this.service.article.find({ category: params.id }) + .select('-category') + .exec() + data.articles = articles + data.articlesCount = articles.length + } + return data + } + + async create () { + const { ctx } = this + const body = this.ctx.validateBody(this.rules.create) + const exists = await this.find({ name: body.name }).exec() + if (exists && exists.length) { + ctx.throw(200, '分类已经存在') + } + return await this.newAndSave(body) + } + + async update () { + const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + const body = this.ctx.validateBody(this.rules.update) + return await this.updateById(params.id, body).exec() + } + + async delete () { + const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + const articles = await this.service.article.find({ category: params.id }).exec() + if (articles && articles.length) { + ctx.throw(200, '该分类下有文章,不能删除') + } + const data = await this.deleteById(params.id).exec() + return data && data.ok && data.n + } +} diff --git a/app/service/comment.js b/app/service/comment.js index f757d74..7ca6147 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -214,7 +214,7 @@ module.exports = class CommentService extends ProxyService { select: 'author meta sticky ups' } ]).exec() - + return data } @@ -246,21 +246,21 @@ module.exports = class CommentService extends ProxyService { location, ip, ua: ctx.req.headers['user-agent'] || '', - referer: ctx.req.headers['referer'] || '' + referer: ctx.req.headers.referer || '' } // 永链 const permalink = this.getPermalink(body) const isSpam = await this.app.akismet.checkSpam({ - user_ip: ip, - user_agent: body.meta.ua, - referrer: body.meta.referer, - permalink, - comment_type: getCommentType(type), - comment_author: user.name, - comment_author_email: user.email, - comment_author_url: user.site, - comment_content: content, - is_test: this.app.config.isProd + user_ip: ip, + user_agent: body.meta.ua, + referrer: body.meta.referer, + permalink, + comment_type: getCommentType(type), + comment_author: user.name, + comment_author_email: user.email, + comment_author_url: user.site, + comment_content: content, + is_test: this.app.config.isProd }) // 如果是Spam评论 if (isSpam) { @@ -308,7 +308,7 @@ module.exports = class CommentService extends ProxyService { if (ctx._isAuthed && ctx._user._id.toString() !== cache.author._id.toString()) { return ctx.fail('其他人的评论内容不能修改') } - + if (body.content !== undefined) { body.renderedContent = this.app.utils.markdown.render(body.content) } @@ -326,17 +326,17 @@ module.exports = class CommentService extends ProxyService { comment_author_email: cache.author.github.email, comment_author_url: cache.author.github.blog, comment_content: cache.content, - is_test: isProd + is_test: this.config.isProd } const SPAM = this.config.modelValidate.comment.state.optional.SPAM - if (cache.state === SPAM && state !== SPAM) { + if (cache.state === SPAM && body.state !== SPAM) { // 垃圾评论转为正常评论 if (cache.spam) { body.spam = false // 报告给Akismet this.app.akismet.submitSpam(opt) } - } else if (cache.state !== SPAM && state === SPAM) { + } else if (cache.state !== SPAM && body.state === SPAM) { // 正常评论转为垃圾评论 if (!cache.spam) { body.spam = true @@ -347,7 +347,7 @@ module.exports = class CommentService extends ProxyService { } let data = null if (!ctx._isAuthed) { - data = await this.updateById(id, comment).select('-content -state -updatedAt') + data = await this.updateById(params.id, body).select('-content -state -updatedAt') .populate({ path: 'author', select: 'github' @@ -359,9 +359,10 @@ module.exports = class CommentService extends ProxyService { .populate({ path: 'forward', select: 'author meta sticky ups' - }).exec() + }) + .exec() } else { - data = await this.updateById(id, comment).exec() + data = await this.updateById(params.id, body).exec() } data ? ctx.success(data, '评论更新成功') @@ -402,7 +403,7 @@ module.exports = class CommentService extends ProxyService { adminType = '评论' } else if (type === commentType.MESSAGE) { // 站内留言 - adminTitle = `个人站点有新的留言` + adminTitle = '个人站点有新的留言' adminType = '留言' } @@ -443,12 +444,12 @@ module.exports = class CommentService extends ProxyService { // 评论类型说明 function getCommentType (type) { - switch (type) { - case 0: - return '文章评论' - case 1: - return '站点留言' - default: - return '评论' - } + switch (type) { + case 0: + return '文章评论' + case 1: + return '站点留言' + default: + return '评论' + } } diff --git a/app/service/proxy.js b/app/service/proxy.js index 4ffa884..374ccd6 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -20,16 +20,16 @@ module.exports = class ProxyService extends Service { return this.model.paginate(query, opt) } - findById (id) { - return this.model.findById(id) + findById (id, select = null, opt = {}) { + return this.model.findById(id, select, opt) } - find (query = {}, opt = {}) { - return this.model.find(query, null, opt) + find (query = {}, select = null, opt = {}) { + return this.model.find(query, select, opt) } - findOne (query = {}, opt = {}) { - return this.model.findOne(query, null, opt) + findOne (query = {}, select = null, opt = {}) { + return this.model.findOne(query, select, opt) } updateById (id, doc, opt = {}) { @@ -67,11 +67,4 @@ module.exports = class ProxyService extends Service { count (query = {}) { return this.model.count(query) } - - res (data, error) { - return { - data, - error - } - } } diff --git a/app/service/proxy2.js b/app/service/proxy2.js new file mode 100644 index 0000000..9912276 --- /dev/null +++ b/app/service/proxy2.js @@ -0,0 +1,42 @@ +/** + * @desc 公共的model proxy service + */ + +const { Service } = require('egg') + +module.exports = class ProxyService extends Service { + init () { + return this.model.init() + } + + getListByQuery (query, select = null, opt) { + return this.model.find(query, select, opt).exec() + } + + getItem (query, select = null, opt) { + opt = this.app.merge({ + lean: true + }, opt) + return this.model.findOne(query, select, opt).exec() + } + + getItemById (id) { + return this.getItem({ _id: id }) + } + + create (data) { + return new this.model(data).save() + } + + updateById (id, data, opt) { + opt = this.app.merge({ + lean: true, + new: true + }) + return this.model.findByIdAndUpdate(id, data, opt).exec() + } + + deleteById (id, opt) { + return this.model.findByIdAndDelete(id, opt).exec() + } +} diff --git a/app/utils/share.js b/app/utils/share.js index 886d685..0d9e829 100644 --- a/app/utils/share.js +++ b/app/utils/share.js @@ -1,5 +1,7 @@ const mongoose = require('mongoose') +exports.lodash = require('lodash') + exports.noop = function () {} // 首字母大写 diff --git a/config/plugin.js b/config/plugin.js index d3bc506..cb4eb0e 100644 --- a/config/plugin.js +++ b/config/plugin.js @@ -25,6 +25,11 @@ exports.redis = { package: 'egg-redis' } +exports.routerPlus = { + enable: true, + package: 'egg-router-plus' +} + exports.akismet = { enable: true, path: path.join(__dirname, '../app/lib/plugin/egg-akismet') diff --git a/package.json b/package.json index 2674c08..e56c9ab 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "egg-console": "^2.0.1", "egg-mongoose": "^3.1.0", "egg-redis": "^2.0.0", + "egg-router-plus": "^1.2.2", "egg-scripts": "^2.5.0", "egg-validate": "^1.1.1", "geoip-lite": "^1.3.2", diff --git a/test/app/controller/home.test.js b/test/app/controller/home.test.js deleted file mode 100644 index 9d8d590..0000000 --- a/test/app/controller/home.test.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const { app, assert } = require('egg-mock/bootstrap'); - -describe('test/app/controller/home.test.js', () => { - - it('should assert', function* () { - const pkg = require('../../../package.json'); - assert(app.config.keys.startsWith(pkg.name)); - - // const ctx = app.mockContext({}); - // yield ctx.service.xx(); - }); - - it('should GET /', () => { - return app.httpRequest() - .get('/') - .expect('hi, egg') - .expect(200); - }); -}); diff --git a/test/app/service/category.test.js b/test/app/service/category.test.js new file mode 100644 index 0000000..cdad034 --- /dev/null +++ b/test/app/service/category.test.js @@ -0,0 +1,66 @@ +const { app, assert } = require('egg-mock/bootstrap') + +describe('test/app/service/category.test.js', () => { + let ctx, + categoryService, + category + + before(() => { + ctx = app.mockContext() + categoryService = ctx.service.category + }) + + it('create pass', async () => { + const name = '测试分类' + const description = '测试分类描述' + const exts = [{ key: 'icon', value: 'fa-fuck' }] + const data = await categoryService.create({ name, description, extends: exts }) + assert(data.name === name) + assert(data.description === description) + assert(data.extends.length === exts.length && data.extends[0].key === exts[0].key && data.extends[0].value === exts[0].value) + category = data + }) + + it('getListByQuery pass', async () => { + const query = {} + const data = await categoryService.getListByQuery(query) + assert.equal(data.every(item => 'count' in item), true) + }) + + it('getItem pass', async () => { + const find = await categoryService.getItem({ name: category.name }) + assert.equal(find._id.toString(), category._id.toString()) + assert.equal(find.name, category.name) + assert.equal(find.description, category.description) + }) + + it('getItemById pass', async () => { + const find = await categoryService.getItemById(category._id) + assert.equal(find._id.toString(), category._id.toString()) + assert.equal(find.name, category.name) + assert.equal(find.description, category.description) + }) + + it('updateById pass', async () => { + const update = { + name: '测试分类修改', + description: '测试分类描述修改', + extends: [{ key: 'icon', value: 'fa-fuck-m' }] + } + const data = await categoryService.updateById(category._id, update) + assert.equal(data._id.toString(), category._id.toString()) + assert.equal(data.name, update.name) + assert.equal(data.description, update.description) + assert(data.extends.length === update.extends.length && data.extends[0].key === update.extends[0].key && data.extends[0].value === update.extends[0].value) + assert.notEqual(data.name, category.name) + assert.notEqual(data.description, category.description) + assert(data.extends[0].key === category.extends[0].key && data.extends[0].value !== category.extends[0].value) + }) + + it('deleteById pass', async () => { + const data = await categoryService.deleteById(category._id) + assert.equal(data._id.toString(), category._id.toString()) + const find = await categoryService.getItemById(category._id) + assert.equal(find, null) + }) +}) From eb0ed2dab65abd831ea0947f3b1824f5087445e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 28 Aug 2018 02:03:45 +0800 Subject: [PATCH 121/208] refactor: refactor tag & setting & user service, and add tag service unit test case --- app/controller/auth.js | 78 ++++++++++++--- app/controller/category.js | 14 +-- app/controller/setting.js | 37 +++++-- app/controller/tag.js | 75 ++++++++++++-- app/controller/user.js | 17 ++-- app/extend/application.js | 8 ++ app/middleware/auth.js | 8 +- app/model/setting.js | 4 - app/model/user.js | 2 +- app/router/backend.js | 1 + app/service/article.js | 2 +- app/service/auth.js | 158 +++++++----------------------- app/service/category.js | 6 +- app/service/categorybak.js | 128 ------------------------ app/service/github.js | 81 +++++++++++++++ app/service/proxy2.js | 2 +- app/service/setting.js | 123 +++++++++-------------- app/service/tag.js | 135 +++++-------------------- app/service/user.js | 47 ++------- config/config.default.js | 5 +- test/app/service/category.test.js | 4 +- test/app/service/tag.test.js | 66 +++++++++++++ 22 files changed, 469 insertions(+), 532 deletions(-) delete mode 100644 app/service/categorybak.js create mode 100644 app/service/github.js create mode 100644 test/app/service/tag.test.js diff --git a/app/controller/auth.js b/app/controller/auth.js index 124f2a1..669a95c 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -7,29 +7,83 @@ const { } = require('egg') module.exports = class AuthController extends Controller { + get rules () { + return { + login: { + username: { type: 'string', required: true }, + password: { type: 'string', required: true } + }, + update: { + name: { type: 'string', required: false }, + email: { type: 'email', required: false }, + site: { type: 'url', required: false }, + description: { type: 'string', required: false }, + avatar: { type: 'string', required: false }, + slogan: { type: 'string', required: false }, + company: { type: 'string', required: false }, + location: { type: 'string', required: false } + }, + password: { + password: { type: 'string', required: true, min: 6 }, + oldPassword: { type: 'string', required: true, min: 6 } + } + } + } + async login () { - await this.service.auth.login() + const { ctx } = this + const body = this.ctx.validateBody(this.rules.login) + const user = await this.service.user.getItem({ name: body.username }) + if (!user) { + return ctx.fail('用户不存在') + } + const vertifyPassword = this.app.utils.encode.bcompare(body.password, user.password) + if (!vertifyPassword) { + return ctx.fail('密码错误') + } + const token = this.service.auth.setCookie(user, true) + this.logger.info(`用户登录成功, ID:${user._id},用户名:${user.name}`) + ctx.success({ id: user._id, token }, '登录成功') } async logout () { - await this.service.auth.logout() + const { ctx } = this + this.service.auth.setCookie(ctx._user, false) + this.logger.info(`用户退出成功, 用户ID:${ctx._user._id},用户名:${ctx._user.name}`) + ctx.success('退出成功') } async info () { - const { ctx } = this - const data = await this.service.auth.info() - if (data.info) { - ctx.success(data) - } else { - ctx.fail(401) - } + this.ctx.success(this.ctx._user, '管理员信息获取成功') } + /** + * @desc 管理员信息更新,不包含密码更新 + */ async update () { const { ctx } = this - const data = await this.service.auth.update() + const body = this.ctx.validateBody(this.rules.update) + const data = await this.service.user.updateById(ctx._user_id, body) + data + ? ctx.success(data, '管理员信息更新成功') + : ctx.fail('管理员信息更新失败') + } + + /** + * @desc 管理员密码更新 + */ + async password () { + const { ctx } = this + const body = this.ctx.validateBody(this.rules.password) + const vertifyPassword = this.app.utils.encode.bcompare(body.oldPassword, ctx._user.password) + if (!vertifyPassword) { + ctx.throw(200, '原密码错误') + } + const data = await this.service.user.updateById(ctx._user._id, { + password: this.app.utils.encode.bhash(body.password) + }) data - ? ctx.success(data, '信息更新成功') - : ctx.fail('信息更新失败') + ? ctx.success(data, '密码更新成功') + : ctx.fail('密码更新失败') } } diff --git a/app/controller/category.js b/app/controller/category.js index 03479b5..eb11ad5 100644 --- a/app/controller/category.js +++ b/app/controller/category.js @@ -51,7 +51,7 @@ module.exports = class CategoryController extends Controller { { name: keywordReg } ] } - const data = await this.service.category.getListByQuery(query) + const data = await this.service.category.getList(query) data ? ctx.success(data, '分类列表获取成功') : ctx.fail('分类列表获取失败') @@ -68,22 +68,22 @@ module.exports = class CategoryController extends Controller { async create () { const { ctx } = this - const body = ctx.validateBody(this.service.category.rules.create) + const body = ctx.validateBody(this.rules.create) const { name } = body - const exists = await this.service.category.getItem({ name }) - if (exists) { + const exist = await this.service.category.getItem({ name }) + if (exist) { return ctx.fail('分类已存在') } const data = await this.service.category.create(body) data - ? ctx.success(data[0], '分类创建成功') + ? ctx.success(data, '分类创建成功') : ctx.fail('分类创建失败') } async update () { const { ctx } = this const params = ctx.validateParamsObjectId() - const body = ctx.validateBody(this.service.category.rules.update) + const body = ctx.validateBody(this.rules.update) const data = await this.service.category.updateById(params.id, body) data ? ctx.success(data, '分类更新成功') @@ -93,7 +93,7 @@ module.exports = class CategoryController extends Controller { async delete () { const { ctx } = this const params = ctx.validateParamsObjectId() - const articles = await this.service.article.getListByQuery({ category: params._id }, 'title') + const articles = await this.service.article.getList({ category: params._id }, 'title') if (articles.length) { return ctx.fail('该分类下还有文章,不能删除', articles) } diff --git a/app/controller/setting.js b/app/controller/setting.js index 3d7d334..08b296c 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -5,19 +5,44 @@ const { Controller } = require('egg') module.exports = class SettingController extends Controller { + get rules () { + return { + index: { + filter: { type: 'string', required: false } + }, + create: { + site: { type: 'object', required: false }, + keys: { type: 'object', required: false } + }, + update: { + site: { type: 'object', required: false }, + keys: { type: 'object', required: false } + } + } + } + async index () { const { ctx } = this - const data = await this.service.setting.index() + ctx.validate(this.rules.index, ctx.query) + const data = await this.service.setting.getItem() data - ? ctx.success(data, '数据获取成功') - : ctx.fail('数据获取失败') + ? ctx.success(data, '配置获取成功') + : ctx.fail('配置获取失败') } async update () { const { ctx } = this - const data = await this.service.setting.update(ctx.request.body) + let body = ctx.validateBody(this.rules.create) + const exist = await this.service.setting.getItem() + if (!exist) { + return ctx.fail('配置未找到') + } + body = this.app.merge(exist, body) + await this.service.setting.updateById(exist._id, body) + // 抓取友链 + const data = await this.service.setting.updateLinks() data - ? ctx.success(data, '数据更新成功') - : ctx.fail('数据更新失败') + ? ctx.success(data, '配置更新成功') + : ctx.fail('配置更新失败') } } diff --git a/app/controller/tag.js b/app/controller/tag.js index a663541..5a8e4d5 100644 --- a/app/controller/tag.js +++ b/app/controller/tag.js @@ -1,14 +1,57 @@ /** - * @desc 标签Controller + * @desc 标签 Controller */ const { Controller } = require('egg') module.exports = class TagController extends Controller { + get rules () { + return { + list: { + // 查询关键词 + keyword: { type: 'string', required: false } + }, + create: { + name: { type: 'string', required: true }, + description: { type: 'string', required: false }, + extends: { + type: 'array', + required: false, + itemType: 'object', + rule: { + key: 'string', + value: 'string' + } + } + }, + update: { + name: { type: 'string', required: false }, + description: { type: 'string', required: false }, + extends: { + type: 'array', + required: false, + itemType: 'object', + rule: { + key: 'string', + value: 'string' + } + } + } + } + } + async list () { const { ctx } = this - const data = await this.service.tag.list() - this.app.utils.share.noop() + ctx.validate(this.rules.list, ctx.query) + const query = {} + const { keyword } = ctx.query + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { name: keywordReg } + ] + } + const data = await this.service.tag.getList(query) data ? ctx.success(data, '标签列表获取成功') : ctx.fail('标签列表获取失败') @@ -16,7 +59,8 @@ module.exports = class TagController extends Controller { async item () { const { ctx } = this - const data = await this.service.tag.item() + const params = ctx.validateParamsObjectId() + const data = await this.service.tag.getItemById(params.id) data ? ctx.success(data, '标签详情获取成功') : ctx.fail('标签详情获取失败') @@ -24,15 +68,23 @@ module.exports = class TagController extends Controller { async create () { const { ctx } = this - const data = await this.service.tag.create() - data && data.length - ? ctx.success(data[0], '标签创建成功') + const body = ctx.validateBody(this.rules.create) + const { name } = body + const exist = await this.service.tag.getItem({ name }) + if (exist) { + return ctx.fail('标签已存在') + } + const data = await this.service.tag.create(body) + data + ? ctx.success(data, '标签创建成功') : ctx.fail('标签创建失败') } async update () { const { ctx } = this - const data = await this.service.tag.update() + const params = ctx.validateParamsObjectId() + const body = ctx.validateBody(this.rules.update) + const data = await this.service.tag.updateById(params.id, body) data ? ctx.success(data, '标签更新成功') : ctx.fail('标签更新失败') @@ -40,7 +92,12 @@ module.exports = class TagController extends Controller { async delete () { const { ctx } = this - const data = await this.service.tag.delete() + const params = ctx.validateParamsObjectId() + const articles = await this.service.article.getList({ tag: params._id }, 'title') + if (articles.length) { + return ctx.fail('该标签下还有文章,不能删除', articles) + } + const data = await this.service.tag.deleteById(params.id) data ? ctx.success('标签删除成功') : ctx.fail('标签删除失败') diff --git a/app/controller/user.js b/app/controller/user.js index fa9a594..5dbe356 100644 --- a/app/controller/user.js +++ b/app/controller/user.js @@ -7,7 +7,11 @@ const { Controller } = require('egg') module.exports = class UserController extends Controller { async list () { const { ctx } = this - const data = await this.service.user.list() + let select = '-password' + if (!ctx._isAuthed) { + select += ' -createdAt -updatedAt -role' + } + const data = await this.service.user.getList({}, select) data ? ctx.success(data, '用户列表获取成功') : ctx.fail('用户列表获取失败') @@ -15,13 +19,14 @@ module.exports = class UserController extends Controller { async item () { const { ctx } = this - const data = await this.service.user.item() + const { id } = ctx.validateParamsObjectId() + let select = '-password' + if (!ctx._isAuthed) { + select += ' -createdAt -updatedAt -github' + } + const data = await this.service.user.getItemById(id, select) data ? ctx.success(data, '用户详情获取成功') : ctx.fail('用户详情获取失败') } - - async update () {} - - async password () {} } diff --git a/app/extend/application.js b/app/extend/application.js index 5915188..be66bfb 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -1,6 +1,8 @@ const mongoosePaginate = require('mongoose-paginate-v2') const lodash = require('lodash') +const prefix = 'http://' + module.exports = { // model schema处理 processSchema (schema, options = {}, middlewares = {}) { @@ -27,5 +29,11 @@ module.exports = { }, merge () { return lodash.merge.apply(null, Array.prototype.slice.call(arguments)) + }, + proxyUrl (url) { + if (lodash.isString(url) && url.startsWith(prefix)) { + return url.replace(prefix, `${this.config.author.url}/proxy/`) + } + return url } } diff --git a/app/middleware/auth.js b/app/middleware/auth.js index 15b53aa..9336246 100644 --- a/app/middleware/auth.js +++ b/app/middleware/auth.js @@ -15,11 +15,11 @@ module.exports = app => { const userId = ctx.cookies.get(app.config.userCookieKey, { signed: false }) - const user = await ctx.service.user.findById(userId).exec() + const user = await ctx.service.user.getItemById(userId, '-password') if (!user) { return ctx.fail(401, '用户不存在') } - ctx._user = user.toObject() + ctx._user = user ctx._isAdmin = user.role === app.config.modelValidate.user.role.optional.ADMIN ctx._isAuthed = true await next() @@ -38,8 +38,8 @@ function verifyToken (app) { try { decodedToken = await jwt.verify(token, config.secrets) } catch (err) { - logger.error('Token校验出错,错误:' + err.message) - return ctx.throw(401, err) + logger.warn('Token校验出错,错误:' + err.message) + return ctx.fail(401, '登录失效') } if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { // 已校验权限 diff --git a/app/model/setting.js b/app/model/setting.js index b5aec1a..f3d4d5c 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -27,10 +27,6 @@ module.exports = app => { }, // 第三方插件的参数 keys: { - // 反垃圾邮件 - akismet: { - apiKey: { type: String, default: '' } - }, // 阿里云oss aliyun: { accessKeyId: { type: String, default: '' }, diff --git a/app/model/user.js b/app/model/user.js index 295e4a7..9179ad2 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -14,7 +14,7 @@ module.exports = app => { site: { type: String, validate: app.utils.validate.isSiteUrl }, slogan: { type: String }, description: { type: String, default: '' }, - // 角色 0 管理员 | 1 普通用户 | 2 github用户,不能更改 + // 角色 0 管理员 | 1 普通用户 role: { type: String, default: userValidateConfig.role.default, diff --git a/app/router/backend.js b/app/router/backend.js index a499089..a3a99d0 100644 --- a/app/router/backend.js +++ b/app/router/backend.js @@ -50,4 +50,5 @@ module.exports = app => { backendRouter.post('/auth/login', controller.auth.login) backendRouter.get('/auth/logout', auth, controller.auth.logout) backendRouter.get('/auth/info', auth, controller.auth.info) + backendRouter.post('/auth/password', auth, controller.auth.password) } diff --git a/app/service/article.js b/app/service/article.js index 5cd045f..01af547 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -53,7 +53,7 @@ module.exports = class ArticleService extends ProxyService { } } - getListByQuery (query, select = null, opt) { + getList (query, select = null, opt) { return this.model.find(query, select, opt).exec() } diff --git a/app/service/auth.js b/app/service/auth.js index 3115b06..3f1c9ef 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -5,135 +5,49 @@ const { Service } = require('egg') module.exports = class AuthService extends Service { - get rules () { - return { - login: { - username: { type: 'string', required: true }, - password: { type: 'string', required: true } - }, - update: { - name: { type: 'string', required: false }, - email: { type: 'email', required: false }, - site: { type: 'url', required: false }, - description: { type: 'string', required: false }, - avatar: { type: 'string', required: false }, - slogan: { type: 'string', required: false }, - company: { type: 'string', required: false }, - location: { type: 'string', required: false } - }, - password: { - password: { type: 'string', required: true }, - oldPassword: { type: 'string', required: true } - } - } - } - - async login () { - const { ctx } = this - const body = this.ctx.validateBody(this.rules.login) - const user = await this.service.user.findOne({ name: body.username }).exec() - if (!user) { - return ctx.fail('用户不存在') - } - const vertifyPassword = this.app.utils.encode.bcompare(body.password, user.password) - if (vertifyPassword) { - const { key, domain, maxAge, signed } = this.app.config.session - const token = this.app.utils.token.sign(this.app, { - id: user._id, - name: user.name - }) - ctx.cookies.set(key, token, { signed, domain, maxAge, httpOnly: false }) - ctx.cookies.set(this.app.config.userCookieKey, user._id, { signed, domain, maxAge, httpOnly: false }) - this.logger.info(`用户登录成功, ID:${user._id},用户名:${user.name}`) - ctx.success({ - id: user._id, - token - }, '登录成功') - } else { - ctx.fail('密码错误') - } - } - - async logout () { - const { ctx } = this - const { key, domain, signed } = this.app.config.session + /** + * @desc 设置cookie,用于登录和退出 + * @param {User} user 登录用户 + * @param {Boolean} isLogin 是否是登录操作 + * @return {String} token 用户token + */ + setCookie (user, isLogin = false) { + const { key, domain, maxAge, signed } = this.app.config.session const token = this.app.utils.token.sign(this.app, { - id: ctx._user._id, - name: ctx._user.name - }, false) - ctx.cookies.set(key, token, { signed, domain, maxAge: 0, httpOnly: false }) - ctx.cookies.set(this.app.config.auth.userCookieKey, ctx._user._id, { signed, domain, maxAge: 0, httpOnly: false }) - this.logger.info(`用户登出成功, 用户ID:${ctx.user._id},用户名:${ctx.user.name}`) - ctx.success('登出成功') - } - - async info () { - const { ctx } = this - const adminId = ctx._user._id - if (!adminId && !ctx._isAuthed) { - return ctx.fail(401) - } - let data = null - if (ctx._isAuthed) { - data = await this.service.user.findById(adminId).select('-password').exec() - } - return { - info: data, - token: ctx.session._token - } - } - - async update () { - const { ctx } = this - const body = this.ctx.validateBody(this.rules.update) - return await this.service.user.updateById(ctx._user_id, body) - } - - async password () { - const { ctx } = this - const body = this.ctx.validateBody(this.rules.password) - const verify = this.app.utils.encode.bcompare(body.oldPassword, ctx._user.password) - if (!verify) { - ctx.throw(200, '原密码错误') - } - return await this.updateById(ctx._user._id, { - password: this.app.utils.encode.bhash(body.password) - }).exec() + id: user._id, + name: user.name + }, isLogin) + this.ctx.cookies.set(key, token, { signed, domain, maxAge: isLogin ? maxAge : 0, httpOnly: false }) + this.ctx.cookies.set(this.app.config.userCookieKey, user._id, { signed, domain, maxAge: isLogin ? maxAge : 0, httpOnly: false }) + return token } + /** + * @desc 创建管理员,用于server初始化时 + */ async seed () { const ADMIN = this.config.modelValidate.user.role.optional.ADMIN - const exist = await this.service.user.findOne({ role: ADMIN }).exec() + const exist = await this.service.user.getItem({ role: ADMIN }) if (!exist) { - await this.create() - } - } - - async create (name) { - const ADMIN = this.config.modelValidate.user.role.optional.ADMIN - const defaultAdmin = this.config.defaultAdmin - const admin = await this.service.common.getGithubUserInfo(name || defaultAdmin.name) - if (!admin) { - return this.logger.warn('管理员创建失败') - } - const data = await this.service.user.create({ - role: ADMIN, - name: admin.name, - email: admin.email || this.config.pkg.author.email, - password: this.app.utils.encode.bhash(defaultAdmin.password), - slogan: admin.bio, - site: admin.blog || admin.url, - avatar: this.service.common.proxyUrl(admin.avatar_url), - company: admin.company, - location: admin.location, - github: { - id: admin.id, - login: admin.login + const defaultAdmin = this.config.defaultAdmin + const admin = await this.service.github.getUserInfo(defaultAdmin.name) + if (admin) { + await this.service.user.create({ + role: ADMIN, + name: admin.name, + email: admin.email || this.config.author.email, + password: this.app.utils.encode.bhash(defaultAdmin.password), + slogan: admin.bio, + site: admin.blog || admin.url, + avatar: this.app.proxyUrl(admin.avatar_url), + company: admin.company, + location: admin.location, + github: { + id: admin.id, + login: admin.login + } + }) } - }) - if (!data) { - return this.logger.warn('管理员创建失败:' + admin.name) } - this.logger.info('管理员创建成功:' + admin.name) } } diff --git a/app/service/category.js b/app/service/category.js index 08341f0..5375fa9 100644 --- a/app/service/category.js +++ b/app/service/category.js @@ -9,7 +9,7 @@ module.exports = class CategoryService extends ProxyService { return this.app.model.Category } - async getListByQuery (query, select = null, opt) { + async getList (query, select = null, opt) { opt = this.app.merge({ sort: '-createdAt' }, opt) @@ -18,7 +18,7 @@ module.exports = class CategoryService extends ProxyService { const PUBLISH = this.app.config.modelValidate.article.state.optional.PUBLISH await Promise.all( categories.map(async item => { - const articles = await this.service.article.getListByQuery({ + const articles = await this.service.article.getList({ category: item._id, state: PUBLISH }) @@ -35,7 +35,7 @@ module.exports = class CategoryService extends ProxyService { }, opt) const category = await this.model.findOne(query, select, opt).exec() if (category) { - category.articles = await this.service.article.getListByQuery({ category: category._id }) + category.articles = await this.service.article.getList({ category: category._id }) } return category } diff --git a/app/service/categorybak.js b/app/service/categorybak.js deleted file mode 100644 index f652f34..0000000 --- a/app/service/categorybak.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @desc 分类 Services - */ - -const ProxyService = require('./proxy') - -module.exports = class CategoryService extends ProxyService { - get model () { - return this.app.model.Category - } - - get rules () { - return { - list: { - // 查询关键词 - keyword: { type: 'string', required: false } - }, - create: { - name: { type: 'string', required: true }, - description: { type: 'string', required: false }, - list: { type: 'number', required: true }, - extends: { - type: 'array', - required: false, - itemType: 'object', - rule: { - key: 'string', - value: 'string' - } - } - }, - update: { - name: { type: 'string', required: false }, - description: { type: 'string', required: false }, - list: { type: 'number', required: true }, - extends: { - type: 'array', - required: false, - itemType: 'object', - rule: { - key: 'string', - value: 'string' - } - } - } - } - } - - async list () { - const { ctx, app, service } = this - ctx.validate(this.rules.list, ctx.query) - const query = {} - const { keyword } = ctx.query - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { name: keywordReg } - ] - } - const data = await this.find(query).sort('-createdAt').exec() - - if (data) { - const isFunction = app.utils.validate.isFunction - const PUBLISH = app.config.modelValidate.article.state.optional.PUBLISH - await Promise.all(data.map((item, index) => { - const toObject = item.toObject - if (isFunction(toObject)) { - item = item.toObject() - } - return service.article.find({ - category: item._id, - state: PUBLISH - }).exec().then(articles => { - item.count = articles.length - data[index] = item - }) - })) - } - - return data - } - - async item () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - let data = await this.findById(params.id).exec() - if (data) { - data = data.toObject() - const articles = await this.service.article.find({ category: params.id }) - .select('-category') - .exec() - data.articles = articles - data.articlesCount = articles.length - } - return data - } - - async create () { - const { ctx } = this - const body = this.ctx.validateBody(this.rules.create) - const exists = await this.find({ name: body.name }).exec() - if (exists && exists.length) { - ctx.throw(200, '分类已经存在') - } - return await this.newAndSave(body) - } - - async update () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - const body = this.ctx.validateBody(this.rules.update) - return await this.updateById(params.id, body).exec() - } - - async delete () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - const articles = await this.service.article.find({ category: params.id }).exec() - if (articles && articles.length) { - ctx.throw(200, '该分类下有文章,不能删除') - } - const data = await this.deleteById(params.id).exec() - return data && data.ok && data.n - } -} diff --git a/app/service/github.js b/app/service/github.js new file mode 100644 index 0000000..512b594 --- /dev/null +++ b/app/service/github.js @@ -0,0 +1,81 @@ +/** + * @desc Github api + */ + +const { Service } = require('egg') + +module.exports = class GithubService extends Service { + /** + * @desc GitHub fetcher + * @param {String} url url + * @param {Object} opt 配置 + * @return {Object} 抓取的结果 + */ + async fetch (url, opt) { + url = 'https://api.github.com' + url + try { + const res = await this.app.curl(url, this.app.merge({ + dataType: 'json', + timeout: 30000, + headers: { + Accept: 'application/json' + } + }, opt)) + if (res && res.status === 200) { + return res.data + } + } catch (error) { + this.logger.error(error) + } + return null + } + + /** + * @desc 获取GitHub用户信息 + * @param {String} username 用户名(GitHub login) + * @return {Object} 用户信息 + */ + async getUserInfo (username) { + if (!username) return null + let gayhub = {} + if (this.config.isLocal) { + // 测试环境下 用测试配置 + gayhub = this.config.github + } else { + const { keys } = this.service.setting.getItem() + if (!keys || !keys.github) { + this.logger.warn('未找到GitHub配置') + return null + } + gayhub = keys.github + } + const { clientID, clientSecret } = gayhub + const data = await this.fetch(`/users/${username}?client_id=${clientID}&client_secret=${clientSecret}`) + if (data) { + this.logger.info(`GitHub用户信息抓取成功:${username}`) + } else { + this.logger.warn(`GitHub用户信息抓取失败:${username}`) + } + return data + } + + /** + * @desc 批量获取GitHub用户信息 + * @param {Array} usernames username array + * @return {Array} 返回数据 + */ + async getUsersInfo (usernames = []) { + if (!Array.isArray(usernames) || !usernames.length) return [] + return await Promise.all(usernames.map(name => this.getUserInfo(name))) + } + + async getAuthUserInfo (access_token) { + const data = await this.fetch(`/user?access_token=${access_token}`) + if (data) { + this.logger.warn('Github用户信息抓取成功') + } else { + this.logger.warn('Github用户信息抓取失败') + } + return data + } +} diff --git a/app/service/proxy2.js b/app/service/proxy2.js index 9912276..6efde9a 100644 --- a/app/service/proxy2.js +++ b/app/service/proxy2.js @@ -9,7 +9,7 @@ module.exports = class ProxyService extends Service { return this.model.init() } - getListByQuery (query, select = null, opt) { + getList (query, select = null, opt) { return this.model.find(query, select, opt).exec() } diff --git a/app/service/setting.js b/app/service/setting.js index 8c10921..8eda851 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -2,77 +2,24 @@ * @desc Setting Services */ -const ProxyService = require('./proxy') +const ProxyService = require('./proxy2') module.exports = class SettingService extends ProxyService { get model () { return this.app.model.Setting } - get rules () { - return { - index: { - filter: { type: 'string', required: false } - }, - create: { - site: { - type: 'object', - required: false - }, - keys: { - type: 'object', - required: false - } - }, - update: { - site: { - type: 'object', - required: false - }, - keys: { - type: 'object', - required: false - } - } - } - } - - async index () { - const { ctx } = this - ctx.validate(this.rules.index, ctx.query) - const query = {} - const { filter } = ctx.query - if (filter) { - query.select = filter.split(',').join(' ') - } - if (!ctx._isAuthed) { - query.select = 'site' - } - return await this.findOne(query).exec() - } - - async keys () { - const data = await this.findOne().select('keys').exec() - return data && data.keys || {} - } - - async create () { - const { ctx } = this - const body = this.ctx.validateBody(this.rules.create, payload) - const exist = await this.findOne().exec() - if (exist) { - ctx.throw(200, '分类已经存在') - } - return await this.newAndSave(body) - } - + /** + * @desc 初始化配置数据,用于server初始化时 + * @return {Setting} 配置数据 + */ async seed () { - const exist = await this.findOne().exec() + const exist = await this.getItem() if (exist) { return exist } - const data = await this.newAndSave() - if (data && data.length) { + const data = await this.create() + if (data) { this.logger.info('Setting初始化成功') } else { this.logger.info('Setting初始化失败') @@ -80,19 +27,45 @@ module.exports = class SettingService extends ProxyService { return data } - async update (payload) { - if (!payload) { - // http request - payload = await this.findOne().exec() - if (!payload) return - } - payload.site = payload.site || {} - // 更新友链 - payload.site.links = await this.service.common.generateLinks(payload.site.links) - const data = await this.updateOne({}, payload).exec() - if (data) { - this.logger.info('Setting更新成功') - } - return data + /** + * @desc 抓取并生成友链 + * @param {Array} links 友链 + * @return {Array} 抓取后的友链 + */ + async generateLinks (links = []) { + if (!links || !links.length) return [] + links = await Promise.all( + links.map(async link => { + if (link) { + const userInfo = await this.service.github.getUserInfo(link.github) + if (userInfo) { + link.avatar = this.app.proxyUrl(userInfo.avatar_url) + link.slogan = userInfo.bio + link.site = link.site || userInfo.blog || userInfo.url + return link + } + } + return null + }) + ) + this.logger.info('友链抓取成功') + return links.filter(item => !!item) + } + + /** + * @desc 更新友链 + * @return {Setting} 更新友链后的配置数据 + */ + async updateLinks () { + let setting = await this.getItem() + if (!setting) return null + const update = await this.generateLinks(setting.site.links) + setting = await this.updateById(setting._id, { + $set: { + 'site.links': update + } + }) + this.logger.info('友链更新成功') + return setting } } diff --git a/app/service/tag.js b/app/service/tag.js index 9dcf1a8..089a5bb 100644 --- a/app/service/tag.js +++ b/app/service/tag.js @@ -1,127 +1,42 @@ /** - * @desc Tag Services + * @desc 标签 Services */ -const ProxyService = require('./proxy') +const ProxyService = require('./proxy2') module.exports = class TagService extends ProxyService { get model () { return this.app.model.Tag } - get rules () { - return { - list: { - // 查询关键词 - keyword: { type: 'string', required: false } - }, - create: { - name: { type: 'string', required: true }, - keyword: { type: 'string', required: false }, - extends: { - type: 'array', - required: false, - itemType: 'object', - rule: { - key: 'string', - value: 'string' - } - } - }, - update: { - name: { type: 'string', required: false }, - keyword: { type: 'string', required: false }, - extends: { - type: 'array', - required: false, - itemType: 'object', - rule: { - key: 'string', - value: 'string' - } - } - } - } - } - - async list () { - const { ctx, app, service } = this - ctx.validate(this.rules.list, ctx.query) - const query = {} - const { keyword } = ctx.query - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { name: keywordReg } - ] - } - const data = await this.find(query).sort('-createdAt').exec() - - if (data) { - const isFunction = app.utils.validate.isFunction - const PUBLISH = app.config.modelValidate.article.state.optional.PUBLISH - await Promise.all(data.map((item, index) => { - const toObject = item.toObject - if (isFunction(toObject)) { - item = item.toObject() - } - return service.article.find({ - tag: item._id, - state: PUBLISH - }).exec().then(articles => { + async getList (query, select = null, opt) { + opt = this.app.merge({ + sort: '-createdAt' + }, opt) + const categories = await this.model.find(query, select, opt).exec() + if (categories.length) { + const PUBLISH = this.app.config.modelValidate.article.state.optional.PUBLISH + await Promise.all( + categories.map(async item => { + const articles = await this.service.article.getList({ + category: item._id, + state: PUBLISH + }) item.count = articles.length - data[index] = item }) - })) - } - - return data - } - - async item () { - const { ctx } = this - const { params } = ctx - ctx.validateObjectId(params) - let data = await this.findById(params.id).exec() - if (data) { - data = data.toObject() - const articles = await this.service.article.find({ tag: params.id }) - .select('-tag') - .exec() - data.articles = articles - data.articlesCount = articles.length + ) } - return data - } - - async create () { - const { ctx } = this - const { body } = ctx.request - ctx.validate(this.rules.create, body) - const exists = await this.find({ name: body.name }).exec() - if (exists && exists.length) { - ctx.throw(200, '标签已经存在') - } - return await this.newAndSave(body) - } - - async update () { - const { ctx } = this - const { params } = ctx - ctx.validateObjectId(params) - const body = this.ctx.validateBody(this.rules.create) - return await this.updateById(params.id, body).exec() + return categories } - async delete () { - const { ctx } = this - const { params } = ctx - ctx.validateObjectId(params) - const articles = await this.service.article.find({ tag: params.id }).exec() - if (articles && articles.length) { - ctx.throw(200, '该标签下有文章,不能删除') + async getItem (query, select = null, opt) { + opt = this.app.merge({ + lean: true + }, opt) + const category = await this.model.findOne(query, select, opt).exec() + if (category) { + category.articles = await this.service.article.getList({ category: category._id }) } - const data = await this.deleteById(params.id).exec() - return data && data.ok && data.n + return category } } diff --git a/app/service/user.js b/app/service/user.js index 792da4a..58e5a8b 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -2,57 +2,28 @@ * @desc User Services */ -const ProxyService = require('./proxy') +const ProxyService = require('./proxy2') module.exports = class UserService extends ProxyService { get model () { return this.app.model.User } - get rules () { - return { - // TODO: - } - } - - async list () { - const { ctx } = this - let select = '-password' - if (!ctx._isAuthed) { - select += ' -createdAt -updatedAt -role' - } - return await this.find() - .sort('-createdAt') - .select(select) - .exec() - } - - async item () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - let select = '-password' - if (!ctx._isAuthed) { - select += ' -createdAt -updatedAt -github' - } - return await this.findById(params.id).select(select).exec() - } - // 创建用户 async create (user) { const { name } = user - const exist = await this.findOne({ name }).exec() + const exist = await this.getItem({ name }) if (exist) { - this.logger.warn(`用户已存在:${name}`) return exist } - const data = await this.newAndSave(user) - if (!data || !data.length) { - this.logger.error(`用户创建失败:${name}`) - return null + const data = await new this.model(user).save() + const type = ['管理员', '用户'][data.role] + if (data) { + this.logger.info(`${type}创建成功:${name}`) + } else { + this.logger.error(`${type}创建失败:${name}`) } - this.logger.error(`用户创建成功:${name}`) - return data[0] + return data } async checkCommentAuthor (author) { diff --git a/config/config.default.js b/config/config.default.js index 08161af..6d9e7f4 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -113,13 +113,12 @@ module.exports = appInfo => { } }, user: { - // 角色 0 管理员 | 1 普通用户 | 2 gayhub用户,不能更改 + // 角色 0 管理员 | 1 普通用户 role: { default: '1', optional: { ADMIN: '0', - NORMAL: '1', - GAYHUB: '2' + NORMAL: '1' } } }, diff --git a/test/app/service/category.test.js b/test/app/service/category.test.js index cdad034..2e36a1e 100644 --- a/test/app/service/category.test.js +++ b/test/app/service/category.test.js @@ -21,9 +21,9 @@ describe('test/app/service/category.test.js', () => { category = data }) - it('getListByQuery pass', async () => { + it('getList pass', async () => { const query = {} - const data = await categoryService.getListByQuery(query) + const data = await categoryService.getList(query) assert.equal(data.every(item => 'count' in item), true) }) diff --git a/test/app/service/tag.test.js b/test/app/service/tag.test.js new file mode 100644 index 0000000..9ac9551 --- /dev/null +++ b/test/app/service/tag.test.js @@ -0,0 +1,66 @@ +const { app, assert } = require('egg-mock/bootstrap') + +describe('test/app/service/tag.test.js', () => { + let ctx, + tagService, + tag + + before(() => { + ctx = app.mockContext() + tagService = ctx.service.tag + }) + + it('create pass', async () => { + const name = '测试标签' + const description = '测试标签描述' + const exts = [{ key: 'icon', value: 'fa-fuck' }] + const data = await tagService.create({ name, description, extends: exts }) + assert(data.name === name) + assert(data.description === description) + assert(data.extends.length === exts.length && data.extends[0].key === exts[0].key && data.extends[0].value === exts[0].value) + tag = data + }) + + it('getList pass', async () => { + const query = {} + const data = await tagService.getList(query) + assert.equal(data.every(item => 'count' in item), true) + }) + + it('getItem pass', async () => { + const find = await tagService.getItem({ name: tag.name }) + assert.equal(find._id.toString(), tag._id.toString()) + assert.equal(find.name, tag.name) + assert.equal(find.description, tag.description) + }) + + it('getItemById pass', async () => { + const find = await tagService.getItemById(tag._id) + assert.equal(find._id.toString(), tag._id.toString()) + assert.equal(find.name, tag.name) + assert.equal(find.description, tag.description) + }) + + it('updateById pass', async () => { + const update = { + name: '测试标签修改', + description: '测试标签描述修改', + extends: [{ key: 'icon', value: 'fa-fuck-m' }] + } + const data = await tagService.updateById(tag._id, update) + assert.equal(data._id.toString(), tag._id.toString()) + assert.equal(data.name, update.name) + assert.equal(data.description, update.description) + assert(data.extends.length === update.extends.length && data.extends[0].key === update.extends[0].key && data.extends[0].value === update.extends[0].value) + assert.notEqual(data.name, tag.name) + assert.notEqual(data.description, tag.description) + assert(data.extends[0].key === tag.extends[0].key && data.extends[0].value !== tag.extends[0].value) + }) + + it('deleteById pass', async () => { + const data = await tagService.deleteById(tag._id) + assert.equal(data._id.toString(), tag._id.toString()) + const find = await tagService.getItemById(tag._id) + assert.equal(find, null) + }) +}) From c82e5566ccbad8f0879dfcc3a137972dfe680a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 28 Aug 2018 20:36:56 +0800 Subject: [PATCH 122/208] refector: article controller refector done --- .eslintrc | 5 +- app.js | 1 + app/controller/article.js | 167 ++++++++- app/controller/auth.js | 4 +- app/controller/category.js | 4 +- app/controller/comment.js | 2 +- app/controller/setting.js | 2 +- app/controller/tag.js | 4 +- app/extend/application.js | 13 + app/lib/plugin/egg-akismet/lib/akismet.js | 8 +- app/lib/plugin/egg-mailer/lib/mailer.js | 2 +- app/model/article.js | 2 +- app/model/setting.js | 6 + app/service/article.js | 299 ++++------------ app/service/articlebak.js | 402 ++++++++++++++++++++++ app/service/category.js | 4 +- app/service/comment.js | 10 +- app/service/proxy.js | 6 +- app/service/proxy2.js | 53 ++- app/service/setting.js | 7 +- app/service/tag.js | 4 +- app/service/user.js | 4 +- app/utils/share.js | 14 - test/app/service/category.test.js | 8 +- test/app/service/tag.test.js | 8 +- 25 files changed, 746 insertions(+), 293 deletions(-) create mode 100644 app/service/articlebak.js diff --git a/.eslintrc b/.eslintrc index ea915a3..e01f267 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,6 +6,7 @@ "space-before-function-paren": [ "error", "always"], "strict": 0, "comma-dangle": 0, - "array-bracket-spacing": 0 + "array-bracket-spacing": 0, + "no-use-before-define": 0 } -} \ No newline at end of file +} diff --git a/app.js b/app.js index 39ccb93..bc4f667 100644 --- a/app.js +++ b/app.js @@ -9,6 +9,7 @@ module.exports = app => { app.beforeStart(async () => { const ctx = app.createAnonymousContext() await ctx.service.setting.seed() + await ctx.service.setting.mountToApp() await ctx.service.auth.seed() }) } diff --git a/app/controller/article.js b/app/controller/article.js index d2777d1..0c3ad52 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -5,9 +5,143 @@ const { Controller } = require('egg') module.exports = class ArticleController extends Controller { + get rules () { + return { + list: { + page: { type: 'number', required: true, min: 1 }, + limit: { type: 'number', required: false, min: 1 }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + category: { type: 'objectId', required: false }, + tag: { type: 'objectId', required: false }, + keyword: { type: 'string', required: false }, + startDate: { type: 'dateTime', required: false }, + endDate: { type: 'dateTime', required: false }, + // -1 desc | 1 asc + order: { type: 'enum', values: [-1, 1], required: false }, + sortBy: { type: 'enum', values: ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], required: false } + }, + item: { + // 后台用,只获取当前文章内容,不获取相关文章和上下篇文章 + single: { type: 'boolean', required: false } + }, + create: { + title: { type: 'string', required: true }, + content: { type: 'string', required: true }, + description: { type: 'string', required: false }, + keywords: { type: 'array', required: false }, + category: { type: 'objectId', required: false }, + tag: { type: 'array', required: false, itemType: 'objectId' }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + thumb: { type: 'url', required: false }, + createdAt: { type: 'dateTime', required: false } + }, + update: { + title: { type: 'string', required: false }, + content: { type: 'string', required: false }, + description: { type: 'string', required: false }, + keywords: { type: 'array', required: false }, + category: { type: 'objectId', required: false }, + tag: { type: 'array', required: false, itemType: 'objectId' }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + thumb: { type: 'url', required: false }, + createdAt: { type: 'dateTime', required: false } + } + } + } + async list () { const { ctx } = this - const data = await this.service.article.list() + ctx.query.page = Number(ctx.query.page) + if (ctx.query.limit) { + ctx.query.limit = Number(ctx.query.limit) + } + ctx.validate(this.rules.list, ctx.query) + const { page, limit, state, keyword, category, tag, order, sortBy, startDate, endDate } = ctx.query + const options = { + sort: { + updatedAt: -1, + createdAt: -1 + }, + page, + limit: limit || this.app.setting.limit.articleCount, + select: '-content -renderedContent', + populate: [ + { + path: 'category', + select: 'name description extends' + }, { + path: 'tag', + select: 'name description' + } + ] + } + const query = {} + if (state !== undefined) { + query.state = state + } + + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { title: keywordReg } + ] + } + + // 分类 + if (category) { + // 如果是id + if (this.app.utils.validate.isObjectId(category)) { + query.category = category + } else { + // 普通字符串,需要先查到id + const c = await this.service.category.getItem({ name: category }) + query.category = c ? c._id : this.app.utils.share.createObjectId() + } + } + + // 标签 + if (tag) { + // 如果是id + if (this.app.utils.validate.isObjectId(tag)) { + query.tag = tag + } else { + // 普通字符串,需要先查到id + const t = await this.service.tag.getItem({ name: tag }) + query.tag = t ? t._id : this.app.utils.share.createObjectId() + } + } + + // 未通过权限校验(前台获取文章列表) + if (!ctx._isAuthed) { + // 将文章状态重置为1 + query.state = 1 + // 文章列表不需要content和state + options.select = '-content -renderedContent -state' + } else { + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } + + // 起始日期 + if (startDate) { + const $gte = new Date(startDate) + if ($gte.toString() !== 'Invalid Date') { + query.createdAt = { $gte } + } + } + + // 结束日期 + if (endDate) { + const $lte = new Date(endDate) + if ($lte.toString() !== 'Invalid Date') { + query.createdAt = Object.assign({}, query.createdAt, { $lte }) + } + } + } + const data = await this.service.article.getLimitListByQuery(query, options) data ? ctx.success(data, '文章列表获取成功') : ctx.fail('文章列表获取失败') @@ -15,7 +149,8 @@ module.exports = class ArticleController extends Controller { async item () { const { ctx } = this - const data = await this.service.article.item() + const params = ctx.validateParamsObjectId() + const data = await this.service.article.getItemById(params.id) data ? ctx.success(data, '文章详情获取成功') : ctx.fail('文章详情获取失败') @@ -23,7 +158,11 @@ module.exports = class ArticleController extends Controller { async create () { const { ctx } = this - const data = await this.service.article.create() + const body = this.ctx.validateBody(this.rules.create) + if (body.createdAt) { + body.createdAt = new Date(body.createdAt) + } + const data = await this.service.article.create(body) data ? ctx.success(data, '文章创建成功') : ctx.fail('文章创建失败') @@ -31,7 +170,17 @@ module.exports = class ArticleController extends Controller { async update () { const { ctx } = this - const data = await this.service.article.update() + const params = ctx.validateParamsObjectId() + const body = ctx.validateBody(this.rules.update) + if (body.createdAt) { + body.createdAt = new Date(body.createdAt) + } + const data = await this.service.article.updateItemById( + params.id, + body, + null, + 'category tag' + ) data ? ctx.success(data, '文章更新成功') : ctx.fail('文章更新失败') @@ -39,7 +188,8 @@ module.exports = class ArticleController extends Controller { async delete () { const { ctx } = this - const data = await this.service.article.delete() + const params = ctx.validateParamsObjectId() + const data = await this.service.article.deleteItemById(params.id) data ? ctx.success('文章删除成功') : ctx.fail('文章删除失败') @@ -47,7 +197,12 @@ module.exports = class ArticleController extends Controller { async like () { const { ctx } = this - const data = await this.service.article.update() + const params = ctx.validateParamsObjectId() + const data = await this.service.article.updateItemById(params.id, { + $inc: { + 'meta.ups': 1 + } + }) data ? ctx.success(data, '文章点赞成功') : ctx.fail('文章点赞失败') diff --git a/app/controller/auth.js b/app/controller/auth.js index 669a95c..e6fde15 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -63,7 +63,7 @@ module.exports = class AuthController extends Controller { async update () { const { ctx } = this const body = this.ctx.validateBody(this.rules.update) - const data = await this.service.user.updateById(ctx._user_id, body) + const data = await this.service.user.updateItemById(ctx._user_id, body) data ? ctx.success(data, '管理员信息更新成功') : ctx.fail('管理员信息更新失败') @@ -79,7 +79,7 @@ module.exports = class AuthController extends Controller { if (!vertifyPassword) { ctx.throw(200, '原密码错误') } - const data = await this.service.user.updateById(ctx._user._id, { + const data = await this.service.user.updateItemById(ctx._user._id, { password: this.app.utils.encode.bhash(body.password) }) data diff --git a/app/controller/category.js b/app/controller/category.js index eb11ad5..0bfdeac 100644 --- a/app/controller/category.js +++ b/app/controller/category.js @@ -84,7 +84,7 @@ module.exports = class CategoryController extends Controller { const { ctx } = this const params = ctx.validateParamsObjectId() const body = ctx.validateBody(this.rules.update) - const data = await this.service.category.updateById(params.id, body) + const data = await this.service.category.updateItemById(params.id, body) data ? ctx.success(data, '分类更新成功') : ctx.fail('分类更新失败') @@ -97,7 +97,7 @@ module.exports = class CategoryController extends Controller { if (articles.length) { return ctx.fail('该分类下还有文章,不能删除', articles) } - const data = await this.service.category.deleteById(params.id) + const data = await this.service.category.deleteItemById(params.id) data ? ctx.success('分类删除成功') : ctx.fail('分类删除失败') diff --git a/app/controller/comment.js b/app/controller/comment.js index e91f024..7450a54 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -26,7 +26,7 @@ module.exports = class CommentController extends Controller { if (data) { if (data.type === this.config.modelValidate.comment.type.optional.COMMENT) { // 如果是文章评论,则更新文章评论数量 - this.service.article.updateArticleCommentCount(data.article) + this.service.article.updateCommentCount(data.article) } // 发送邮件通知站主和被评论者 this.service.comment.sendCommentEmailToAdminAndUser(data) diff --git a/app/controller/setting.js b/app/controller/setting.js index 08b296c..89cdf96 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -38,7 +38,7 @@ module.exports = class SettingController extends Controller { return ctx.fail('配置未找到') } body = this.app.merge(exist, body) - await this.service.setting.updateById(exist._id, body) + await this.service.setting.updateItemById(exist._id, body) // 抓取友链 const data = await this.service.setting.updateLinks() data diff --git a/app/controller/tag.js b/app/controller/tag.js index 5a8e4d5..ecf5c34 100644 --- a/app/controller/tag.js +++ b/app/controller/tag.js @@ -84,7 +84,7 @@ module.exports = class TagController extends Controller { const { ctx } = this const params = ctx.validateParamsObjectId() const body = ctx.validateBody(this.rules.update) - const data = await this.service.tag.updateById(params.id, body) + const data = await this.service.tag.updateItemById(params.id, body) data ? ctx.success(data, '标签更新成功') : ctx.fail('标签更新失败') @@ -97,7 +97,7 @@ module.exports = class TagController extends Controller { if (articles.length) { return ctx.fail('该标签下还有文章,不能删除', articles) } - const data = await this.service.tag.deleteById(params.id) + const data = await this.service.tag.deleteItemById(params.id) data ? ctx.success('标签删除成功') : ctx.fail('标签删除失败') diff --git a/app/extend/application.js b/app/extend/application.js index be66bfb..d659d8f 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -35,5 +35,18 @@ module.exports = { return url.replace(prefix, `${this.config.author.url}/proxy/`) } return url + }, + // 获取分页请求的响应数据 + getDocsPaginationData (docs) { + if (!docs) return null + return { + list: docs.docs, + pageInfo: { + total: docs.totalDocs, + current: docs.page > docs.totalPages ? docs.totalPages : docs.page, + pages: docs.totalPages, + limit: docs.limit + } + } } } diff --git a/app/lib/plugin/egg-akismet/lib/akismet.js b/app/lib/plugin/egg-akismet/lib/akismet.js index 7f0d813..5276fb2 100644 --- a/app/lib/plugin/egg-akismet/lib/akismet.js +++ b/app/lib/plugin/egg-akismet/lib/akismet.js @@ -21,17 +21,17 @@ let isValidKey = false /** * @desc Akismet Client Class - * @param {String} [required] key Akismet apikey - * @param {String} [required] blog Akismet blog + * @param {String} key Akismet apikey + * @param {String} blog Akismet blog */ class AkismetClient { constructor (config, app) { this.config = config this.app = app - this.initClient() + this.init() } - initClient () { + init () { this.client = akismet.client(this.config) } diff --git a/app/lib/plugin/egg-mailer/lib/mailer.js b/app/lib/plugin/egg-mailer/lib/mailer.js index 77707d9..672493d 100644 --- a/app/lib/plugin/egg-mailer/lib/mailer.js +++ b/app/lib/plugin/egg-mailer/lib/mailer.js @@ -4,7 +4,7 @@ module.exports = app => { app.addSingleton('mailer', createClient) } -function createClient (config, app) { +function createClient (config) { return { client: null, getClient (opt) { diff --git a/app/model/article.js b/app/model/article.js index 5c188ae..2807f10 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -58,7 +58,7 @@ module.exports = app => { const { content, state } = this._update const find = await this.findOne() if (find) { - if (content !== find.content) { + if (content && content !== find.content) { this._update.renderedContent = app.utils.markdown.render(content) } if (['title', 'content'].some(key => this._update[key] !== find[key])) { diff --git a/app/model/setting.js b/app/model/setting.js index f3d4d5c..92323b8 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -44,6 +44,12 @@ module.exports = app => { clientID: { type: String, default: '' }, clientSecret: { type: String, default: '' } } + }, + limit: { + articleCount: { type: Number, default: 10 }, + relatedArticleCount: { type: Number, default: 10 }, + hotArticleCount: { type: Number, default: 7 }, + commentSpamMaxCount: { type: Number, default: 3 } } }) diff --git a/app/service/article.js b/app/service/article.js index 01af547..7080770 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -2,7 +2,7 @@ * @desc Article Services */ -const ProxyService = require('./proxy') +const ProxyService = require('./proxy2') module.exports = class ArticleService extends ProxyService { get model () { @@ -53,153 +53,24 @@ module.exports = class ArticleService extends ProxyService { } } - getList (query, select = null, opt) { - return this.model.find(query, select, opt).exec() - } - async getLimitListByQuery (query, opt) { - opt = this.app.merge({ - sort: { - updatedAt: -1, - createdAt: -1 - }, - page: 1, - limit: 10, - lean: true, - select: '-content -renderedContent', - populate: [ - { - path: 'category', - select: 'name description extends' - }, { - path: 'tag', - select: 'name description' - } - ] - }, opt) + opt = Object.assign({ lean: true }, opt) const data = await this.model.paginate(query, opt) - return this.app.utils.share.getDocsPaginationData(data) - } - - async list () { - const { ctx } = this - ctx.query.page = Number(ctx.query.page) - ctx.query.limit = Number(ctx.query.limit) - ctx.validate(this.rules.list, ctx.query) - const { page, limit, state, keyword, category, tag, order, sortBy, startDate, endDate } = ctx.query - const options = { - sort: { - updatedAt: -1, - createdAt: -1 - }, - page, - limit, - lean: true, - select: '-content -renderedContent', - populate: [ - { - path: 'category', - select: 'name description extends' - }, { - path: 'tag', - select: 'name description' - } - ] - } - const query = {} - if (state !== undefined) { - query.state = state - } - - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { title: keywordReg } - ] - } - - // 分类 - if (category) { - // 如果是id - if (this.app.utils.validate.isObjectId(category)) { - query.category = category - } else { - // 普通字符串,需要先查到id - const c = await this.service.category.findOne({ name: category }).exec() - query.category = c ? c._id : this.app.utils.share.createObjectId() - } - } - - // 标签 - if (tag) { - // 如果是id - if (this.app.utils.validate.isObjectId(tag)) { - query.tag = tag - } else { - // 普通字符串,需要先查到id - const t = await this.service.tag.findOne({ name: tag }).exec() - query.tag = t ? t._id : this.app.utils.share.createObjectId() - } - } - - // 未通过权限校验(前台获取文章列表) - if (!ctx._isAuthed) { - // 将文章状态重置为1 - query.state = 1 - // 文章列表不需要content和state - options.select = '-content -renderedContent -state' - } else { - // 排序 - if (sortBy && order) { - options.sort = {} - options.sort[sortBy] = order - } - - // 起始日期 - if (startDate) { - const $gte = new Date(startDate) - if ($gte.toString() !== 'Invalid Date') { - query.createdAt = { $gte } - } - } - - // 结束日期 - if (endDate) { - const $lte = new Date(endDate) - if ($lte.toString() !== 'Invalid Date') { - query.createdAt = Object.assign({}, query.createdAt, { $lte }) - } - } - } - - const data = await this.paginate(query, options) - return this.app.utils.share.getDocsPaginationData(data) + return this.app.getDocsPaginationData(data) } - async item () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - ctx.validate(this.rules.item, ctx.query) - let query = null - // 只有前台博客访问文章的时候pv才+1 - if (!ctx._isAuthed) { - query = this.updateOne({ _id: params.id, state: this.config.modelValidate.article.state.optional.PUBLISH }, { $inc: { 'meta.pvs': 1 } }).select('-content') - } else { - query = this.findById(params.id) + async getItemById (id, select, opt = {}, single = false) { + let api = this.getItem.bind(this) + const query = { _id: id } + if (!this.ctx._isAuthed) { + api = this.updateItem.bind(this) + // 前台博客访问文章的时候pv+1 + query.state = this.config.modelValidate.article.state.optional.PUBLISH + select += ' -content' + opt.$inc = { 'meta.pvs': 1 } } - const data = await query.populate([ - { - path: 'category', - select: 'name description extends' - }, { - path: 'tag', - select: 'name description extends' - } - ]).exec() - - if (data && !ctx.query.single) { + const data = await api(query, select, opt) + if (data && !single) { // 获取相关文章和上下篇文章 const [related, adjacent] = await Promise.all([ this.getRelatedArticles(data), @@ -211,42 +82,11 @@ module.exports = class ArticleService extends ProxyService { return data } - async create () { - const body = this.ctx.validateBody(this.rules.create) - if (body.createdAt) { - body.createdAt = new Date(body.createdAt) - } - let data = await this.newAndSave(body) - if (data && data.length) { - data = data[0].toObject() - } - return data - } - - async update () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - const body = ctx.validateBody(this.rules.update) - if (body.createdAt) { - body.createdAt = new Date(body.createdAt) - } - return await this.updateById(params.id, body).populate('category tag').exec() - } - - async delete () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - const data = await this.deleteById(params.id).exec() - return data && data.ok && data.n - } - async like () { const { ctx } = this const { params } = ctx ctx.validateParamsObjectId() - return await this.updateById(params.id, { + return await this.updateItemById(params.id, { $inc: { 'meta.ups': 1 } @@ -318,66 +158,77 @@ module.exports = class ArticleService extends ProxyService { async getRelatedArticles (data) { if (!data || !data._id) return null const { _id, tag = [] } = data - const articles = await this.article.find({ - _id: { $nin: [ _id ] }, - state: 1, - tag: { $in: tag.map(t => t._id) } - }) - .select('title thumb createdAt publishedAt meta category') - .populate({ + const articles = await this.getList( + { + _id: { $nin: [ _id ] }, + state: data.state, + tag: { $in: tag.map(t => t._id) } + }, + 'title thumb createdAt publishedAt meta category', + null, + { path: 'category', select: 'name description' - }) - .exec() - .catch(err => { - this.logger.error('相关文章查询失败,错误:' + err.message) - return null - }) - return articles && articles.slice(0, this.config.limit.relatedArticleLimit) || null + } + ).catch(err => { + this.logger.error('相关文章查询失败,错误:' + err.message) + return null + }) + return articles && articles.slice(0, this.app.setting.limit.relatedArticleCount) || null } // 获取相邻的文章 - async getAdjacentArticles (ctx, data) { + async getAdjacentArticles (data) { if (!data || !data._id) return null - const query = {} + const query = { + createdAt: { + $lt: data.createdAt + } + } // 如果未通过权限校验,将文章状态重置为1 - if (!ctx._isAuthed) { + if (!this.ctx._isAuthed) { query.state = this.config.modelValidate.article.state.optional.PUBLISH } - const prev = await this.service.article.findOne(query) - .select('title createdAt publishedAt thumb category') - .populate({ + const prev = await this.getItem( + query, + 'title createdAt publishedAt thumb category', + { + sort: 'createdAt' + }, + { path: 'category', select: 'name description' - }) - .sort('-createdAt') - .lt('createdAt', data.createdAt) - .exec() - .catch(err => { - this.logger.error('前一篇文章获取失败,错误:' + err.message) - return null - }) - const next = await this.service.article.findOne(query) - .select('title createdAt publishedAt thumb category') - .populate({ + } + ).catch(err => { + this.logger.error('前一篇文章获取失败,错误:' + err.message) + return null + }) + query.createdAt = { + $gt: data.createdAt + } + const next = await this.getItem( + query, + 'title createdAt publishedAt thumb category', + { + sort: 'createdAt' + }, + { path: 'category', select: 'name description' - }) - .sort('createdAt') - .gt('createdAt', data.createdAt) - .exec() - .catch(err => { - this.logger.error('后一篇文章获取失败,错误:' + err.message) - return null - }) + } + ).catch(err => { + this.logger.error('后一篇文章获取失败,错误:' + err.message) + return null + }) return { - prev: prev ? prev.toObject() : null, - next: next ? next.toObject() : null + prev: prev || null, + next: next || null } } - async updateArticleCommentCount (articleIds = []) { - if (!this.app.utils.validate.isArray(articleIds)) { + + async updateCommentCount (articleIds = []) { + if (!Array.isArray(articleIds)) { articleIds = [articleIds] } if (!articleIds.length) return @@ -387,14 +238,12 @@ module.exports = class ArticleService extends ProxyService { const counts = await this.service.comment.aggregate([ { $match: { state: 1, article: { $in: articleIds } } }, { $group: { _id: '$article', total_count: { $sum: 1 } } } - ]) - .exec() - .catch(err => { - this.logger.error('更新文章评论数量前聚合评论数据操作失败,错误:' + err.message) - return [] - }) + ]).catch(err => { + this.logger.error('更新文章评论数量前聚合评论数据操作失败,错误:' + err.message) + return [] + }) Promise.all( - counts.map(count => articleProxy.updateById(count._id, { $set: { 'meta.comments': count.total_count } }).exec()) + counts.map(count => this.updateItemById(count._id, { $set: { 'meta.comments': count.total_count } })) ) .then(() => this.logger.info('文章评论数量更新成功')) .catch(err => this.logger.error('文章评论数量更新失败,错误:' + err.message)) diff --git a/app/service/articlebak.js b/app/service/articlebak.js new file mode 100644 index 0000000..c24ec0e --- /dev/null +++ b/app/service/articlebak.js @@ -0,0 +1,402 @@ +/** + * @desc Article Services + */ + +const ProxyService = require('./proxy') + +module.exports = class ArticleService extends ProxyService { + get model () { + return this.app.model.Article + } + + get rules () { + return { + list: { + page: { type: 'number', required: true, min: 1 }, + limit: { type: 'number', required: true, min: 1 }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + category: { type: 'objectId', required: false }, + tag: { type: 'objectId', required: false }, + keyword: { type: 'string', required: false }, + startDate: { type: 'dateTime', required: false }, + endDate: { type: 'dateTime', required: false }, + // -1 desc | 1 asc + order: { type: 'enum', values: [-1, 1], required: false }, + sortBy: { type: 'enum', values: ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], required: false } + }, + item: { + // 后台用,只获取当前文章内容,不获取相关文章和上下篇文章 + single: { type: 'boolean', required: false } + }, + create: { + title: { type: 'string', required: true }, + content: { type: 'string', required: true }, + description: { type: 'string', required: false }, + keywords: { type: 'array', required: false }, + category: { type: 'objectId', required: false }, + tag: { type: 'array', required: false, itemType: 'objectId' }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + thumb: { type: 'url', required: false }, + createdAt: { type: 'dateTime', required: false } + }, + update: { + title: { type: 'string', required: false }, + content: { type: 'string', required: false }, + description: { type: 'string', required: false }, + keywords: { type: 'array', required: false }, + category: { type: 'objectId', required: false }, + tag: { type: 'array', required: false, itemType: 'objectId' }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + thumb: { type: 'url', required: false }, + createdAt: { type: 'dateTime', required: false } + } + } + } + + getList (query, select = null, opt) { + return this.model.find(query, select, opt).exec() + } + + async getLimitListByQuery (query, opt) { + opt = this.app.merge({ + sort: { + updatedAt: -1, + createdAt: -1 + }, + page: 1, + limit: 10, + lean: true, + select: '-content -renderedContent', + populate: [ + { + path: 'category', + select: 'name description extends' + }, { + path: 'tag', + select: 'name description' + } + ] + }, opt) + const data = await this.model.paginate(query, opt) + return this.app.utils.share.getDocsPaginationData(data) + } + + async list () { + const { ctx } = this + ctx.query.page = Number(ctx.query.page) + ctx.query.limit = Number(ctx.query.limit) + ctx.validate(this.rules.list, ctx.query) + const { page, limit, state, keyword, category, tag, order, sortBy, startDate, endDate } = ctx.query + const options = { + sort: { + updatedAt: -1, + createdAt: -1 + }, + page, + limit, + lean: true, + select: '-content -renderedContent', + populate: [ + { + path: 'category', + select: 'name description extends' + }, { + path: 'tag', + select: 'name description' + } + ] + } + const query = {} + if (state !== undefined) { + query.state = state + } + + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { title: keywordReg } + ] + } + + // 分类 + if (category) { + // 如果是id + if (this.app.utils.validate.isObjectId(category)) { + query.category = category + } else { + // 普通字符串,需要先查到id + const c = await this.service.category.findOne({ name: category }).exec() + query.category = c ? c._id : this.app.utils.share.createObjectId() + } + } + + // 标签 + if (tag) { + // 如果是id + if (this.app.utils.validate.isObjectId(tag)) { + query.tag = tag + } else { + // 普通字符串,需要先查到id + const t = await this.service.tag.findOne({ name: tag }).exec() + query.tag = t ? t._id : this.app.utils.share.createObjectId() + } + } + + // 未通过权限校验(前台获取文章列表) + if (!ctx._isAuthed) { + // 将文章状态重置为1 + query.state = 1 + // 文章列表不需要content和state + options.select = '-content -renderedContent -state' + } else { + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } + + // 起始日期 + if (startDate) { + const $gte = new Date(startDate) + if ($gte.toString() !== 'Invalid Date') { + query.createdAt = { $gte } + } + } + + // 结束日期 + if (endDate) { + const $lte = new Date(endDate) + if ($lte.toString() !== 'Invalid Date') { + query.createdAt = Object.assign({}, query.createdAt, { $lte }) + } + } + } + + const data = await this.paginate(query, options) + return this.app.utils.share.getDocsPaginationData(data) + } + + async item () { + const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + ctx.validate(this.rules.item, ctx.query) + let query = null + // 只有前台博客访问文章的时候pv才+1 + if (!ctx._isAuthed) { + query = this.updateOne({ _id: params.id, state: this.config.modelValidate.article.state.optional.PUBLISH }, { $inc: { 'meta.pvs': 1 } }).select('-content') + } else { + query = this.findById(params.id) + } + const data = await query.populate([ + { + path: 'category', + select: 'name description extends' + }, { + path: 'tag', + select: 'name description extends' + } + ]).exec() + + if (data && !ctx.query.single) { + // 获取相关文章和上下篇文章 + const [related, adjacent] = await Promise.all([ + this.getRelatedArticles(data), + this.getAdjacentArticles(data) + ]) + data.related = related + data.adjacent = adjacent + } + return data + } + + async create () { + const body = this.ctx.validateBody(this.rules.create) + if (body.createdAt) { + body.createdAt = new Date(body.createdAt) + } + let data = await this.newAndSave(body) + if (data && data.length) { + data = data[0].toObject() + } + return data + } + + async update () { + const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + const body = ctx.validateBody(this.rules.update) + if (body.createdAt) { + body.createdAt = new Date(body.createdAt) + } + return await this.updateItemById(params.id, body).populate('category tag').exec() + } + + async delete () { + const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + const data = await this.deleteItemById(params.id).exec() + return data && data.ok && data.n + } + + async like () { + const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + return await this.updateItemById(params.id, { + $inc: { + 'meta.ups': 1 + } + }) + } + + async archives () { + const $match = {} + const $project = { + year: { $year: '$createdAt' }, + month: { $month: '$createdAt' }, + title: 1, + createdAt: 1 + } + if (!this.ctx._isAuthed) { + $match.state = 1 + } else { + $project.state = 1 + } + let data = await this.aggregate([ + { $match }, + { $sort: { createdAt: 1 } }, + { $project }, + { + $group: { + _id: { + year: '$year', + month: '$month' + }, + articles: { + $push: { + title: '$title', + _id: '$_id', + createdAt: '$createdAt', + state: '$state' + } + } + } + } + ]) + let count = 0 + if (data && data.length) { + data = [...new Set(data.map(item => item._id.year))].map(year => { + const months = [] + data.forEach(item => { + const { _id, articles } = item + if (year === _id.year) { + count += articles.length + months.push({ + month: _id.month, + monthStr: this.app.utils.share.getMonthFromNum(_id.month), + articles + }) + } + }) + return { + year, + months + } + }) + } + return { + count, + list: data || [] + } + } + + // 根据标签获取相关文章 + async getRelatedArticles (data) { + if (!data || !data._id) return null + const { _id, tag = [] } = data + const articles = await this.article.find({ + _id: { $nin: [ _id ] }, + state: 1, + tag: { $in: tag.map(t => t._id) } + }) + .select('title thumb createdAt publishedAt meta category') + .populate({ + path: 'category', + select: 'name description' + }) + .exec() + .catch(err => { + this.logger.error('相关文章查询失败,错误:' + err.message) + return null + }) + return articles && articles.slice(0, this.config.limit.relatedArticleLimit) || null + } + + // 获取相邻的文章 + async getAdjacentArticles (ctx, data) { + if (!data || !data._id) return null + const query = {} + // 如果未通过权限校验,将文章状态重置为1 + if (!ctx._isAuthed) { + query.state = this.config.modelValidate.article.state.optional.PUBLISH + } + const prev = await this.service.article.findOne(query) + .select('title createdAt publishedAt thumb category') + .populate({ + path: 'category', + select: 'name description' + }) + .sort('-createdAt') + .lt('createdAt', data.createdAt) + .exec() + .catch(err => { + this.logger.error('前一篇文章获取失败,错误:' + err.message) + return null + }) + const next = await this.service.article.findOne(query) + .select('title createdAt publishedAt thumb category') + .populate({ + path: 'category', + select: 'name description' + }) + .sort('createdAt') + .gt('createdAt', data.createdAt) + .exec() + .catch(err => { + this.logger.error('后一篇文章获取失败,错误:' + err.message) + return null + }) + return { + prev: prev ? prev.toObject() : null, + next: next ? next.toObject() : null + } + } + + async updateCommentCount (articleIds = []) { + if (!this.app.utils.validate.isArray(articleIds)) { + articleIds = [articleIds] + } + if (!articleIds.length) return + const { validate, share } = this.app.utils + // TIP: 这里必须$in的是一个ObjectId对象数组,而不能只是id字符串数组 + articleIds = [...new Set(articleIds)].filter(id => validate.isObjectId(id)).map(id => share.createObjectId(id)) + const counts = await this.service.comment.aggregate([ + { $match: { state: 1, article: { $in: articleIds } } }, + { $group: { _id: '$article', total_count: { $sum: 1 } } } + ]) + .exec() + .catch(err => { + this.logger.error('更新文章评论数量前聚合评论数据操作失败,错误:' + err.message) + return [] + }) + Promise.all( + counts.map(count => articleProxy.updateItemById(count._id, { $set: { 'meta.comments': count.total_count } }).exec()) + ) + .then(() => this.logger.info('文章评论数量更新成功')) + .catch(err => this.logger.error('文章评论数量更新失败,错误:' + err.message)) + } +} diff --git a/app/service/category.js b/app/service/category.js index 5375fa9..35f4db9 100644 --- a/app/service/category.js +++ b/app/service/category.js @@ -29,11 +29,11 @@ module.exports = class CategoryService extends ProxyService { return categories } - async getItem (query, select = null, opt) { + async getItemById (id, select = null, opt) { opt = this.app.merge({ lean: true }, opt) - const category = await this.model.findOne(query, select, opt).exec() + const category = await this.model.findById(id, select, opt).exec() if (category) { category.articles = await this.service.article.getList({ category: category._id }) } diff --git a/app/service/comment.js b/app/service/comment.js index 7ca6147..f98d294 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -174,7 +174,7 @@ module.exports = class CommentService extends ProxyService { }) })) comments.docs = data - return this.app.utils.share.getDocsPaginationData(comments) + return this.app.getDocsPaginationData(comments) } async item () { @@ -347,7 +347,7 @@ module.exports = class CommentService extends ProxyService { } let data = null if (!ctx._isAuthed) { - data = await this.updateById(params.id, body).select('-content -state -updatedAt') + data = await this.updateItemById(params.id, body).select('-content -state -updatedAt') .populate({ path: 'author', select: 'github' @@ -362,7 +362,7 @@ module.exports = class CommentService extends ProxyService { }) .exec() } else { - data = await this.updateById(params.id, body).exec() + data = await this.updateItemById(params.id, body).exec() } data ? ctx.success(data, '评论更新成功') @@ -373,7 +373,7 @@ module.exports = class CommentService extends ProxyService { const { ctx } = this const { params } = ctx ctx.validateParamsObjectId() - const data = await this.deleteById(params.id).exec() + const data = await this.deleteItemById(params.id).exec() return data && data.ok && data.n } @@ -381,7 +381,7 @@ module.exports = class CommentService extends ProxyService { const { ctx } = this const { params } = ctx ctx.validateParamsObjectId() - return await this.updateById(params.id, { + return await this.updateItemById(params.id, { $inc: { ups: 1 } diff --git a/app/service/proxy.js b/app/service/proxy.js index 374ccd6..5690503 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -32,7 +32,7 @@ module.exports = class ProxyService extends Service { return this.model.findOne(query, select, opt) } - updateById (id, doc, opt = {}) { + updateItemById (id, doc, opt = {}) { return this.model.findByIdAndUpdate(id, doc, Object.assign({ new: true }, opt)) } @@ -48,11 +48,11 @@ module.exports = class ProxyService extends Service { return this.model.remove(query) } - deleteById (id = '') { + deleteItemById (id = '') { return this.model.deleteOne({ _id: id }) } - deleteByIds (ids = []) { + deleteItemByIds (ids = []) { return this.model.deleteMany({ _id: { $in: ids diff --git a/app/service/proxy2.js b/app/service/proxy2.js index 6efde9a..56d4c0a 100644 --- a/app/service/proxy2.js +++ b/app/service/proxy2.js @@ -9,34 +9,69 @@ module.exports = class ProxyService extends Service { return this.model.init() } - getList (query, select = null, opt) { - return this.model.find(query, select, opt).exec() + getList (query, select = null, opt, populate = []) { + const Q = this.model.find(query, select, opt) + if (populate) { + [].concat(populate).forEach(item => Q.populate(item)) + } + return Q.exec() } - getItem (query, select = null, opt) { + getItem (query, select = null, opt, populate = []) { opt = this.app.merge({ lean: true }, opt) - return this.model.findOne(query, select, opt).exec() + const Q = this.model.findOne(query, select, opt) + if (populate) { + [].concat(populate).forEach(item => Q.populate(item)) + } + return Q.exec() } - getItemById (id) { - return this.getItem({ _id: id }) + getItemById (id, select = null, opt, populate = []) { + opt = this.app.merge({ + lean: true + }, opt) + const Q = this.model.findById(id, select, opt) + if (populate) { + [].concat(populate).forEach(item => Q.populate(item)) + } + return Q.exec() } create (data) { return new this.model(data).save() } - updateById (id, data, opt) { + updateItem (query = {}, data, opt, populate = []) { opt = this.app.merge({ lean: true, new: true }) - return this.model.findByIdAndUpdate(id, data, opt).exec() + const Q = this.model.findOneAndUpdate(query, data, opt) + if (populate) { + [].concat(populate).forEach(item => Q.populate(item)) + } + return Q.exec() } - deleteById (id, opt) { + updateItemById (id, data, opt, populate = []) { + opt = this.app.merge({ + lean: true, + new: true + }) + const Q = this.model.findByIdAndUpdate(id, data, opt) + if (populate) { + [].concat(populate).forEach(item => Q.populate(item)) + } + return Q.exec() + } + + deleteItemById (id, opt) { return this.model.findByIdAndDelete(id, opt).exec() } + + aggregate (pipeline = []) { + return this.model.aggregate(pipeline) + } } diff --git a/app/service/setting.js b/app/service/setting.js index 8eda851..1a945fa 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -60,7 +60,7 @@ module.exports = class SettingService extends ProxyService { let setting = await this.getItem() if (!setting) return null const update = await this.generateLinks(setting.site.links) - setting = await this.updateById(setting._id, { + setting = await this.updateItemById(setting._id, { $set: { 'site.links': update } @@ -68,4 +68,9 @@ module.exports = class SettingService extends ProxyService { this.logger.info('友链更新成功') return setting } + + async mountToApp () { + const setting = await this.getItem() + this.app.setting = setting || null + } } diff --git a/app/service/tag.js b/app/service/tag.js index 089a5bb..e815039 100644 --- a/app/service/tag.js +++ b/app/service/tag.js @@ -29,11 +29,11 @@ module.exports = class TagService extends ProxyService { return categories } - async getItem (query, select = null, opt) { + async getItem (id, select = null, opt) { opt = this.app.merge({ lean: true }, opt) - const category = await this.model.findOne(query, select, opt).exec() + const category = await this.model.findById(id, select, opt).exec() if (category) { category.articles = await this.service.article.getList({ category: category._id }) } diff --git a/app/service/user.js b/app/service/user.js index 58e5a8b..fb343b2 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -42,7 +42,7 @@ module.exports = class UserService extends ProxyService { if (author.id) { // 更新 if (isObjectId(author.id)) { - user = await this.updateById(author.id, update).exec() + user = await this.updateItemById(author.id, update).exec() if (user) { this.logger.info('用户更新成功:' + user.name) } @@ -63,7 +63,7 @@ module.exports = class UserService extends ProxyService { const spams = comments.filter(c => c.spam) if (spams.length >= this.config.limit.commentSpamLimit) { if (!user.mute) { - await this.updateById(user._id, { mute: true }).exec() + await this.updateItemById(user._id, { mute: true }).exec() this.logger.info(`用户【${user.name}】禁言成功`) } return false diff --git a/app/utils/share.js b/app/utils/share.js index 0d9e829..9dea0b4 100644 --- a/app/utils/share.js +++ b/app/utils/share.js @@ -11,20 +11,6 @@ exports.createObjectId = (id = '') => { return id ? mongoose.Types.ObjectId(id) : mongoose.Types.ObjectId() } -// 获取分页请求的响应数据 -exports.getDocsPaginationData = docs => { - if (!docs) return null - return { - list: docs.docs, - pageInfo: { - total: docs.totalDocs, - current: docs.page > docs.totalPages ? docs.totalPages : docs.page, - pages: docs.totalPages, - limit: docs.limit - } - } -} - exports.getMonthFromNum = (num = 1) => { return ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][num - 1] || '' } diff --git a/test/app/service/category.test.js b/test/app/service/category.test.js index 2e36a1e..7d0e14b 100644 --- a/test/app/service/category.test.js +++ b/test/app/service/category.test.js @@ -41,13 +41,13 @@ describe('test/app/service/category.test.js', () => { assert.equal(find.description, category.description) }) - it('updateById pass', async () => { + it('updateItemById pass', async () => { const update = { name: '测试分类修改', description: '测试分类描述修改', extends: [{ key: 'icon', value: 'fa-fuck-m' }] } - const data = await categoryService.updateById(category._id, update) + const data = await categoryService.updateItemById(category._id, update) assert.equal(data._id.toString(), category._id.toString()) assert.equal(data.name, update.name) assert.equal(data.description, update.description) @@ -57,8 +57,8 @@ describe('test/app/service/category.test.js', () => { assert(data.extends[0].key === category.extends[0].key && data.extends[0].value !== category.extends[0].value) }) - it('deleteById pass', async () => { - const data = await categoryService.deleteById(category._id) + it('deleteItemById pass', async () => { + const data = await categoryService.deleteItemById(category._id) assert.equal(data._id.toString(), category._id.toString()) const find = await categoryService.getItemById(category._id) assert.equal(find, null) diff --git a/test/app/service/tag.test.js b/test/app/service/tag.test.js index 9ac9551..644fd7b 100644 --- a/test/app/service/tag.test.js +++ b/test/app/service/tag.test.js @@ -41,13 +41,13 @@ describe('test/app/service/tag.test.js', () => { assert.equal(find.description, tag.description) }) - it('updateById pass', async () => { + it('updateItemById pass', async () => { const update = { name: '测试标签修改', description: '测试标签描述修改', extends: [{ key: 'icon', value: 'fa-fuck-m' }] } - const data = await tagService.updateById(tag._id, update) + const data = await tagService.updateItemById(tag._id, update) assert.equal(data._id.toString(), tag._id.toString()) assert.equal(data.name, update.name) assert.equal(data.description, update.description) @@ -57,8 +57,8 @@ describe('test/app/service/tag.test.js', () => { assert(data.extends[0].key === tag.extends[0].key && data.extends[0].value !== tag.extends[0].value) }) - it('deleteById pass', async () => { - const data = await tagService.deleteById(tag._id) + it('deleteItemById pass', async () => { + const data = await tagService.deleteItemById(tag._id) assert.equal(data._id.toString(), tag._id.toString()) const find = await tagService.getItemById(tag._id) assert.equal(find, null) From 67c22694ee74ba8e53a01156fa5f1ef9234e24f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 29 Aug 2018 02:11:21 +0800 Subject: [PATCH 123/208] refactor: comment controller refactor done --- app/controller/article.js | 4 +- app/controller/category.js | 3 + app/controller/comment.js | 304 +++++++++++++++++++++++++- app/controller/setting.js | 2 + app/controller/tag.js | 3 + app/extend/context.js | 13 ++ app/model/article.js | 4 +- app/model/comment.js | 6 +- app/model/setting.js | 1 + app/service/article.js | 71 +----- app/service/articlebak.js | 402 ---------------------------------- app/service/category.js | 13 +- app/service/comment.js | 429 ++++--------------------------------- app/service/common.js | 119 ---------- app/service/mail.js | 41 ++++ app/service/proxy.js | 93 ++++---- app/service/proxy2.js | 77 ------- app/service/setting.js | 5 +- app/service/tag.js | 13 +- app/service/user.js | 43 ++-- config/config.default.js | 2 +- 21 files changed, 501 insertions(+), 1147 deletions(-) delete mode 100644 app/service/articlebak.js delete mode 100644 app/service/common.js create mode 100644 app/service/mail.js delete mode 100644 app/service/proxy2.js diff --git a/app/controller/article.js b/app/controller/article.js index 0c3ad52..ba7e8a6 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -158,7 +158,7 @@ module.exports = class ArticleController extends Controller { async create () { const { ctx } = this - const body = this.ctx.validateBody(this.rules.create) + const body = ctx.validateBody(this.rules.create) if (body.createdAt) { body.createdAt = new Date(body.createdAt) } @@ -204,7 +204,7 @@ module.exports = class ArticleController extends Controller { } }) data - ? ctx.success(data, '文章点赞成功') + ? ctx.success('文章点赞成功') : ctx.fail('文章点赞失败') } diff --git a/app/controller/category.js b/app/controller/category.js index 0bfdeac..dc4b05b 100644 --- a/app/controller/category.js +++ b/app/controller/category.js @@ -61,6 +61,9 @@ module.exports = class CategoryController extends Controller { const { ctx } = this const params = ctx.validateParamsObjectId() const data = await this.service.category.getItemById(params.id) + if (data) { + data.articles = await this.service.article.getList({ category: data._id }) + } data ? ctx.success(data, '分类详情获取成功') : ctx.fail('分类详情获取失败') diff --git a/app/controller/comment.js b/app/controller/comment.js index 7450a54..31eec32 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -5,9 +5,157 @@ const { Controller } = require('egg') module.exports = class CommentController extends Controller { + get rules () { + return { + list: { + page: { type: 'number', required: true, min: 1 }, + limit: { type: 'number', required: false, min: 1 }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, + type: { type: 'enum', values: Object.values(this.config.modelValidate.comment.type.optional), required: false }, + author: { type: 'objectId', required: false }, + article: { type: 'objectId', required: false }, + parent: { type: 'objectId', required: false }, + keyword: { type: 'string', required: false }, + // 时间区间查询仅后台可用,且依赖于createdAt + startDate: { type: 'dateTime', required: false }, + endDate: { type: 'dateTime', required: false }, + // 排序仅后台能用,且order和sortBy需同时传入才起作用 + // -1 desc | 1 asc + order: { type: 'enum', values: [-1, 1], required: false }, + sortBy: { type: 'enum', values: ['createdAt', 'updatedAt', 'ups'], required: false } + }, + create: { + content: { type: 'string', required: true }, + type: { type: 'enum', values: Object.values(this.config.modelValidate.comment.type.optional), required: true }, + article: { type: 'objectId', required: false }, + parent: { type: 'objectId', required: false }, + forward: { type: 'objectId', required: false } + }, + update: { + content: { type: 'string', required: false }, + state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, + sticky: { type: 'boolean', required: false } + } + } + } + async list () { const { ctx } = this - const data = await this.service.comment.list() + ctx.query.page = Number(ctx.query.page) + if (ctx.query.limit) { + ctx.query.limit = Number(ctx.query.limit) + } + ctx.validate(this.rules.list, ctx.query) + const { page, limit, state, keyword, author, article, parent, order, sortBy, startDate, endDate } = ctx.query + // 过滤条件 + const options = { + sort: { createdAt: 1 }, + page, + limit: limit || this.app.setting.limit.commentCount, + select: '', + populate: [ + { + path: 'author', + select: !ctx._isAuthed ? 'github avatar name site' : '' + }, + { + path: 'parent', + select: 'author meta sticky ups', + match: !ctx._isAuthed && { + state: 1 + } || null + }, + { + path: 'forward', + select: 'author meta sticky ups', + match: !ctx._isAuthed && { + state: 1 + } || null, + populate: { + path: 'author', + select: 'avatar github name' + } + } + ] + } + + // 查询条件 + const query = {} + if (state !== undefined) { + query.state = state + } + + // 搜索关键词 + if (keyword) { + const keywordReg = new RegExp(keyword) + query.$or = [ + { title: keywordReg } + ] + } + + // 用户 + if (author) { + // 如果是id + if (this.app.utils.validate.isObjectId(author)) { + query.author = author + } else { + // 普通字符串,需要先查到id + const u = await this.service.user.getItem({ name: author }) + query.author = u ? u._id : this.app.utils.share.createObjectId() + } + } + + // 文章 + if (article) { + // 如果是id + if (this.app.utils.validate.isObjectId(article)) { + query.article = article + } else { + // 普通字符串,需要先查到id + const a = await this.service.article.getItem({ name: article }) + query.article = a ? a._id : this.app.utils.share.createObjectId() + } + } + + if (parent) { + // 获取子评论 + query.parent = parent + } else { + // 获取父评论 + query.parent = { $exists: false } + } + + // 未通过权限校验(前台获取文章列表) + if (!ctx._isAuthed) { + // 将评论状态重置为1 + query.state = 1 + query.spam = false + // 评论列表不需要content和state + options.select = '-content -state -updatedAt -spam -type' + } else { + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } + + // 起始日期 + if (startDate) { + const $gte = new Date(startDate) + if ($gte.toString() !== 'Invalid Date') { + query.createdAt = { $gte } + } + } + + // 结束日期 + if (endDate) { + const $lte = new Date(endDate) + if ($lte.toString() !== 'Invalid Date') { + query.createdAt = Object.assign({}, query.createdAt, { $lte }) + } + } + } + const data = await this.service.comment.getLimitListByQuery(query, options) data ? ctx.success(data, '评论列表获取成功') : ctx.fail('评论列表获取失败') @@ -15,31 +163,164 @@ module.exports = class CommentController extends Controller { async item () { const { ctx } = this - const data = await this.service.comment.item() + const params = ctx.validateParamsObjectId() + const data = await this.service.comment.getItemById(params.id) data ? ctx.success(data, '评论详情获取成功') : ctx.fail('评论详情获取失败') } async create () { - const data = await this.service.comment.create() - if (data) { - if (data.type === this.config.modelValidate.comment.type.optional.COMMENT) { + const { ctx } = this + ctx.validateCommentAuthor() + const body = ctx.validateBody(this.rules.create) + const COMMENT = this.config.modelValidate.comment.type.optional.COMMENT + body.author = ctx.request.body.author + const { article, parent, forward, type, content, author } = body + if (type === COMMENT) { + if (!article) { + return ctx.fail(422, '缺少文章ID') + } + } + if ((parent && !forward) || (!parent && forward)) { + return ctx.fail(422, '缺少parent和forward参数') + } + const user = await this.service.user.checkCommentAuthor(author) + if (!user) { + return ctx.fail('用户不存在') + } else if (user.mute) { + // 被禁言 + return ctx.fail('该用户已被禁言') + } + body.author = user._id + const spamValid = await this.service.user.checkUserSpam(user) + if (!spamValid) { + return ctx.fail('该用户的垃圾评论数量已达到最大限制,已被禁言') + } + const { ip, location } = ctx.getLocation() + const meta = body.meta = { + location, + ip, + ua: ctx.req.headers['user-agent'] || '', + referer: ctx.req.headers.referer || '' + } + // 永链 + const permalink = this.service.comment.getPermalink(body) + const isSpam = await this.app.akismet.checkSpam({ + user_ip: ip, + user_agent: meta.ua, + referrer: meta.referer, + permalink, + comment_type: this.service.comment.getCommentType(type), + comment_author: user.name, + comment_author_email: user.email, + comment_author_url: user.site, + comment_content: content, + is_test: this.config.isProd + }) + // 如果是Spam评论 + if (isSpam) { + return ctx.fail('检测为垃圾评论,该评论将不会显示') + } + this.logger.info('评论检测正常,可以发布') + body.renderedContent = this.app.utils.markdown.render(body.content) + const comment = await this.service.comment.create(body) + if (comment) { + const data = await this.service.comment.getItemById(comment._id) + if (data.type === COMMENT) { // 如果是文章评论,则更新文章评论数量 this.service.article.updateCommentCount(data.article) } // 发送邮件通知站主和被评论者 this.service.comment.sendCommentEmailToAdminAndUser(data) + ctx.success(data, data.type === COMMENT ? '评论发布成功' : '留言发布成功') + } else { + ctx.fail('发布失败') } } async update () { - await this.service.comment.update() + const { ctx } = this + const { params } = ctx + ctx.validateParamsObjectId() + ctx.validateCommentAuthor() + const body = ctx.validateBody(this.rules.create) + body.author = ctx.request.body.author + const exist = await this.getItemById(params.id) + if (!exist) { + return ctx.fail('评论不存在') + } + if (ctx._isAuthed && ctx._user._id !== exist.author._id) { + return ctx.fail('非本人评论不能修改') + } + // 状态修改是涉及到spam修改 + if (body.state !== undefined) { + const permalink = this.service.comment.getPermalink(exist) + const opt = { + user_ip: exist.meta.ip, + user_agent: exist.meta.ua, + referrer: exist.meta.referer, + permalink, + comment_type: this.service.comment.getCommentType(exist.type), + comment_author: exist.author.github.login, + comment_author_email: exist.author.github.email, + comment_author_url: exist.author.github.blog, + comment_content: exist.content, + is_test: this.config.isProd + } + const SPAM = this.config.modelValidate.comment.state.optional.SPAM + if (exist.state === SPAM && body.state !== SPAM) { + // 垃圾评论转为正常评论 + if (exist.spam) { + body.spam = false + // 异步报告给Akismet + this.app.akismet.submitSpam(opt) + } + } else if (exist.state !== SPAM && body.state === SPAM) { + // 正常评论转为垃圾评论 + if (!exist.spam) { + body.spam = true + // 异步报告给Akismet + this.app.akismet.submitHam(opt) + } + } + } + body.renderedContent = this.app.utils.markdown.render(body.content) + let data = null + if (!ctx._isAuthed) { + data = await this.service.comment.updateItemById( + params.id, + body, + null, + [ + { + path: 'author', + select: 'github' + }, { + path: 'parent', + select: 'author meta sticky ups' + }, { + path: 'forward', + select: 'author meta sticky ups' + } + ] + ) + } else { + data = await this.updateItemById(params.id, body) + } + data + ? ctx.success(data, '评论更新成功') + : ctx.fail('评论更新失败') } async delete () { const { ctx } = this - const data = await this.service.comment.delete() + const params = ctx.validateParamsObjectId() + const data = await this.service.comment.deleteItemById(params.id) + if (data.type === this.config.modelValidate.comment.type.optional.COMMENT) { + // 异步 如果是文章评论,则更新文章评论数量 + this.service.article.updateCommentCount(data.article) + } data ? ctx.success('评论删除成功') : ctx.fail('评论删除失败') @@ -47,9 +328,14 @@ module.exports = class CommentController extends Controller { async like () { const { ctx } = this - const data = await this.service.comment.update() + const params = ctx.validateParamsObjectId() + const data = await this.service.comment.updateItemById(params.id, { + $inc: { + ups: 1 + } + }) data - ? ctx.success(data, '评论点赞成功') + ? ctx.success('评论点赞成功') : ctx.fail('评论点赞失败') } } diff --git a/app/controller/setting.js b/app/controller/setting.js index 89cdf96..4a207a4 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -41,6 +41,8 @@ module.exports = class SettingController extends Controller { await this.service.setting.updateItemById(exist._id, body) // 抓取友链 const data = await this.service.setting.updateLinks() + // 更新配置后需要挂载到app上 + await this.service.setting.mountToApp() data ? ctx.success(data, '配置更新成功') : ctx.fail('配置更新失败') diff --git a/app/controller/tag.js b/app/controller/tag.js index ecf5c34..6205348 100644 --- a/app/controller/tag.js +++ b/app/controller/tag.js @@ -61,6 +61,9 @@ module.exports = class TagController extends Controller { const { ctx } = this const params = ctx.validateParamsObjectId() const data = await this.service.tag.getItemById(params.id) + if (data) { + data.articles = await this.service.article.getList({ tag: data._id }) + } data ? ctx.success(data, '标签详情获取成功') : ctx.fail('标签详情获取失败') diff --git a/app/extend/context.js b/app/extend/context.js index 0a41648..a045986 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -23,6 +23,19 @@ module.exports = { } }) }, + validateCommentAuthor (author) { + author = author || this.request.body.author + const { isObjectId, isObject } = this.app.utils.validate + if (isObject(author)) { + this.validate({ + name: 'string', + email: 'string', + site: { type: 'string', required: false } + }, author) + } else if (!isObjectId(author)) { + this.throw(422, '发布人不存在') + } + }, getLocation () { const req = this.req const ip = (req.headers['x-forwarded-for'] || diff --git a/app/model/article.js b/app/model/article.js index 2807f10..279ad8d 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -49,7 +49,9 @@ module.exports = app => { }, { pre: { save (next) { - this.renderedContent = app.utils.markdown.render(this.content) + if (this.content) { + this.renderedContent = app.utils.markdown.render(this.content) + } next() }, async findOneAndUpdate () { diff --git a/app/model/comment.js b/app/model/comment.js index 1c77924..d3ca223 100644 --- a/app/model/comment.js +++ b/app/model/comment.js @@ -57,7 +57,9 @@ module.exports = app => { }, { pre: { save (next) { - this.renderedContent = app.utils.markdown.render(this.content) + if (this.content) { + this.renderedContent = app.utils.markdown.render(this.content) + } next() }, async findOneAndUpdate () { @@ -65,7 +67,7 @@ module.exports = app => { const { content } = this._update const find = await this.findOne() if (find) { - if (content !== find.content) { + if (content && content !== find.content) { this._update.renderedContent = app.utils.markdown.render(content) this._update.updatedAt = Date.now() } diff --git a/app/model/setting.js b/app/model/setting.js index 92323b8..a6c555c 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -47,6 +47,7 @@ module.exports = app => { }, limit: { articleCount: { type: Number, default: 10 }, + commentCount: { type: Number, default: 20 }, relatedArticleCount: { type: Number, default: 10 }, hotArticleCount: { type: Number, default: 7 }, commentSpamMaxCount: { type: Number, default: 3 } diff --git a/app/service/article.js b/app/service/article.js index 7080770..ccaa437 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -2,63 +2,13 @@ * @desc Article Services */ -const ProxyService = require('./proxy2') +const ProxyService = require('./proxy') module.exports = class ArticleService extends ProxyService { get model () { return this.app.model.Article } - get rules () { - return { - list: { - page: { type: 'number', required: true, min: 1 }, - limit: { type: 'number', required: true, min: 1 }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, - category: { type: 'objectId', required: false }, - tag: { type: 'objectId', required: false }, - keyword: { type: 'string', required: false }, - startDate: { type: 'dateTime', required: false }, - endDate: { type: 'dateTime', required: false }, - // -1 desc | 1 asc - order: { type: 'enum', values: [-1, 1], required: false }, - sortBy: { type: 'enum', values: ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], required: false } - }, - item: { - // 后台用,只获取当前文章内容,不获取相关文章和上下篇文章 - single: { type: 'boolean', required: false } - }, - create: { - title: { type: 'string', required: true }, - content: { type: 'string', required: true }, - description: { type: 'string', required: false }, - keywords: { type: 'array', required: false }, - category: { type: 'objectId', required: false }, - tag: { type: 'array', required: false, itemType: 'objectId' }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, - thumb: { type: 'url', required: false }, - createdAt: { type: 'dateTime', required: false } - }, - update: { - title: { type: 'string', required: false }, - content: { type: 'string', required: false }, - description: { type: 'string', required: false }, - keywords: { type: 'array', required: false }, - category: { type: 'objectId', required: false }, - tag: { type: 'array', required: false, itemType: 'objectId' }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, - thumb: { type: 'url', required: false }, - createdAt: { type: 'dateTime', required: false } - } - } - } - - async getLimitListByQuery (query, opt) { - opt = Object.assign({ lean: true }, opt) - const data = await this.model.paginate(query, opt) - return this.app.getDocsPaginationData(data) - } - async getItemById (id, select, opt = {}, single = false) { let api = this.getItem.bind(this) const query = { _id: id } @@ -82,17 +32,6 @@ module.exports = class ArticleService extends ProxyService { return data } - async like () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - return await this.updateItemById(params.id, { - $inc: { - 'meta.ups': 1 - } - }) - } - async archives () { const $match = {} const $project = { @@ -235,13 +174,11 @@ module.exports = class ArticleService extends ProxyService { const { validate, share } = this.app.utils // TIP: 这里必须$in的是一个ObjectId对象数组,而不能只是id字符串数组 articleIds = [...new Set(articleIds)].filter(id => validate.isObjectId(id)).map(id => share.createObjectId(id)) + const PASS = this.config.modelValidate.comment.state.optional.PASS const counts = await this.service.comment.aggregate([ - { $match: { state: 1, article: { $in: articleIds } } }, + { $match: { state: PASS, article: { $in: articleIds } } }, { $group: { _id: '$article', total_count: { $sum: 1 } } } - ]).catch(err => { - this.logger.error('更新文章评论数量前聚合评论数据操作失败,错误:' + err.message) - return [] - }) + ]) Promise.all( counts.map(count => this.updateItemById(count._id, { $set: { 'meta.comments': count.total_count } })) ) diff --git a/app/service/articlebak.js b/app/service/articlebak.js deleted file mode 100644 index c24ec0e..0000000 --- a/app/service/articlebak.js +++ /dev/null @@ -1,402 +0,0 @@ -/** - * @desc Article Services - */ - -const ProxyService = require('./proxy') - -module.exports = class ArticleService extends ProxyService { - get model () { - return this.app.model.Article - } - - get rules () { - return { - list: { - page: { type: 'number', required: true, min: 1 }, - limit: { type: 'number', required: true, min: 1 }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, - category: { type: 'objectId', required: false }, - tag: { type: 'objectId', required: false }, - keyword: { type: 'string', required: false }, - startDate: { type: 'dateTime', required: false }, - endDate: { type: 'dateTime', required: false }, - // -1 desc | 1 asc - order: { type: 'enum', values: [-1, 1], required: false }, - sortBy: { type: 'enum', values: ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], required: false } - }, - item: { - // 后台用,只获取当前文章内容,不获取相关文章和上下篇文章 - single: { type: 'boolean', required: false } - }, - create: { - title: { type: 'string', required: true }, - content: { type: 'string', required: true }, - description: { type: 'string', required: false }, - keywords: { type: 'array', required: false }, - category: { type: 'objectId', required: false }, - tag: { type: 'array', required: false, itemType: 'objectId' }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, - thumb: { type: 'url', required: false }, - createdAt: { type: 'dateTime', required: false } - }, - update: { - title: { type: 'string', required: false }, - content: { type: 'string', required: false }, - description: { type: 'string', required: false }, - keywords: { type: 'array', required: false }, - category: { type: 'objectId', required: false }, - tag: { type: 'array', required: false, itemType: 'objectId' }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, - thumb: { type: 'url', required: false }, - createdAt: { type: 'dateTime', required: false } - } - } - } - - getList (query, select = null, opt) { - return this.model.find(query, select, opt).exec() - } - - async getLimitListByQuery (query, opt) { - opt = this.app.merge({ - sort: { - updatedAt: -1, - createdAt: -1 - }, - page: 1, - limit: 10, - lean: true, - select: '-content -renderedContent', - populate: [ - { - path: 'category', - select: 'name description extends' - }, { - path: 'tag', - select: 'name description' - } - ] - }, opt) - const data = await this.model.paginate(query, opt) - return this.app.utils.share.getDocsPaginationData(data) - } - - async list () { - const { ctx } = this - ctx.query.page = Number(ctx.query.page) - ctx.query.limit = Number(ctx.query.limit) - ctx.validate(this.rules.list, ctx.query) - const { page, limit, state, keyword, category, tag, order, sortBy, startDate, endDate } = ctx.query - const options = { - sort: { - updatedAt: -1, - createdAt: -1 - }, - page, - limit, - lean: true, - select: '-content -renderedContent', - populate: [ - { - path: 'category', - select: 'name description extends' - }, { - path: 'tag', - select: 'name description' - } - ] - } - const query = {} - if (state !== undefined) { - query.state = state - } - - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { title: keywordReg } - ] - } - - // 分类 - if (category) { - // 如果是id - if (this.app.utils.validate.isObjectId(category)) { - query.category = category - } else { - // 普通字符串,需要先查到id - const c = await this.service.category.findOne({ name: category }).exec() - query.category = c ? c._id : this.app.utils.share.createObjectId() - } - } - - // 标签 - if (tag) { - // 如果是id - if (this.app.utils.validate.isObjectId(tag)) { - query.tag = tag - } else { - // 普通字符串,需要先查到id - const t = await this.service.tag.findOne({ name: tag }).exec() - query.tag = t ? t._id : this.app.utils.share.createObjectId() - } - } - - // 未通过权限校验(前台获取文章列表) - if (!ctx._isAuthed) { - // 将文章状态重置为1 - query.state = 1 - // 文章列表不需要content和state - options.select = '-content -renderedContent -state' - } else { - // 排序 - if (sortBy && order) { - options.sort = {} - options.sort[sortBy] = order - } - - // 起始日期 - if (startDate) { - const $gte = new Date(startDate) - if ($gte.toString() !== 'Invalid Date') { - query.createdAt = { $gte } - } - } - - // 结束日期 - if (endDate) { - const $lte = new Date(endDate) - if ($lte.toString() !== 'Invalid Date') { - query.createdAt = Object.assign({}, query.createdAt, { $lte }) - } - } - } - - const data = await this.paginate(query, options) - return this.app.utils.share.getDocsPaginationData(data) - } - - async item () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - ctx.validate(this.rules.item, ctx.query) - let query = null - // 只有前台博客访问文章的时候pv才+1 - if (!ctx._isAuthed) { - query = this.updateOne({ _id: params.id, state: this.config.modelValidate.article.state.optional.PUBLISH }, { $inc: { 'meta.pvs': 1 } }).select('-content') - } else { - query = this.findById(params.id) - } - const data = await query.populate([ - { - path: 'category', - select: 'name description extends' - }, { - path: 'tag', - select: 'name description extends' - } - ]).exec() - - if (data && !ctx.query.single) { - // 获取相关文章和上下篇文章 - const [related, adjacent] = await Promise.all([ - this.getRelatedArticles(data), - this.getAdjacentArticles(data) - ]) - data.related = related - data.adjacent = adjacent - } - return data - } - - async create () { - const body = this.ctx.validateBody(this.rules.create) - if (body.createdAt) { - body.createdAt = new Date(body.createdAt) - } - let data = await this.newAndSave(body) - if (data && data.length) { - data = data[0].toObject() - } - return data - } - - async update () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - const body = ctx.validateBody(this.rules.update) - if (body.createdAt) { - body.createdAt = new Date(body.createdAt) - } - return await this.updateItemById(params.id, body).populate('category tag').exec() - } - - async delete () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - const data = await this.deleteItemById(params.id).exec() - return data && data.ok && data.n - } - - async like () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - return await this.updateItemById(params.id, { - $inc: { - 'meta.ups': 1 - } - }) - } - - async archives () { - const $match = {} - const $project = { - year: { $year: '$createdAt' }, - month: { $month: '$createdAt' }, - title: 1, - createdAt: 1 - } - if (!this.ctx._isAuthed) { - $match.state = 1 - } else { - $project.state = 1 - } - let data = await this.aggregate([ - { $match }, - { $sort: { createdAt: 1 } }, - { $project }, - { - $group: { - _id: { - year: '$year', - month: '$month' - }, - articles: { - $push: { - title: '$title', - _id: '$_id', - createdAt: '$createdAt', - state: '$state' - } - } - } - } - ]) - let count = 0 - if (data && data.length) { - data = [...new Set(data.map(item => item._id.year))].map(year => { - const months = [] - data.forEach(item => { - const { _id, articles } = item - if (year === _id.year) { - count += articles.length - months.push({ - month: _id.month, - monthStr: this.app.utils.share.getMonthFromNum(_id.month), - articles - }) - } - }) - return { - year, - months - } - }) - } - return { - count, - list: data || [] - } - } - - // 根据标签获取相关文章 - async getRelatedArticles (data) { - if (!data || !data._id) return null - const { _id, tag = [] } = data - const articles = await this.article.find({ - _id: { $nin: [ _id ] }, - state: 1, - tag: { $in: tag.map(t => t._id) } - }) - .select('title thumb createdAt publishedAt meta category') - .populate({ - path: 'category', - select: 'name description' - }) - .exec() - .catch(err => { - this.logger.error('相关文章查询失败,错误:' + err.message) - return null - }) - return articles && articles.slice(0, this.config.limit.relatedArticleLimit) || null - } - - // 获取相邻的文章 - async getAdjacentArticles (ctx, data) { - if (!data || !data._id) return null - const query = {} - // 如果未通过权限校验,将文章状态重置为1 - if (!ctx._isAuthed) { - query.state = this.config.modelValidate.article.state.optional.PUBLISH - } - const prev = await this.service.article.findOne(query) - .select('title createdAt publishedAt thumb category') - .populate({ - path: 'category', - select: 'name description' - }) - .sort('-createdAt') - .lt('createdAt', data.createdAt) - .exec() - .catch(err => { - this.logger.error('前一篇文章获取失败,错误:' + err.message) - return null - }) - const next = await this.service.article.findOne(query) - .select('title createdAt publishedAt thumb category') - .populate({ - path: 'category', - select: 'name description' - }) - .sort('createdAt') - .gt('createdAt', data.createdAt) - .exec() - .catch(err => { - this.logger.error('后一篇文章获取失败,错误:' + err.message) - return null - }) - return { - prev: prev ? prev.toObject() : null, - next: next ? next.toObject() : null - } - } - - async updateCommentCount (articleIds = []) { - if (!this.app.utils.validate.isArray(articleIds)) { - articleIds = [articleIds] - } - if (!articleIds.length) return - const { validate, share } = this.app.utils - // TIP: 这里必须$in的是一个ObjectId对象数组,而不能只是id字符串数组 - articleIds = [...new Set(articleIds)].filter(id => validate.isObjectId(id)).map(id => share.createObjectId(id)) - const counts = await this.service.comment.aggregate([ - { $match: { state: 1, article: { $in: articleIds } } }, - { $group: { _id: '$article', total_count: { $sum: 1 } } } - ]) - .exec() - .catch(err => { - this.logger.error('更新文章评论数量前聚合评论数据操作失败,错误:' + err.message) - return [] - }) - Promise.all( - counts.map(count => articleProxy.updateItemById(count._id, { $set: { 'meta.comments': count.total_count } }).exec()) - ) - .then(() => this.logger.info('文章评论数量更新成功')) - .catch(err => this.logger.error('文章评论数量更新失败,错误:' + err.message)) - } -} diff --git a/app/service/category.js b/app/service/category.js index 35f4db9..7c74d9c 100644 --- a/app/service/category.js +++ b/app/service/category.js @@ -2,7 +2,7 @@ * @desc 分类 Services */ -const ProxyService = require('./proxy2') +const ProxyService = require('./proxy') module.exports = class CategoryService extends ProxyService { get model () { @@ -28,15 +28,4 @@ module.exports = class CategoryService extends ProxyService { } return categories } - - async getItemById (id, select = null, opt) { - opt = this.app.merge({ - lean: true - }, opt) - const category = await this.model.findById(id, select, opt).exec() - if (category) { - category.articles = await this.service.article.getList({ category: category._id }) - } - return category - } } diff --git a/app/service/comment.js b/app/service/comment.js index f98d294..89b2106 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -9,203 +9,12 @@ module.exports = class CommentService extends ProxyService { return this.app.model.Comment } - get rules () { - return { - list: { - page: { type: 'number', required: true, min: 1 }, - limit: { type: 'number', required: true, min: 1 }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, - type: { type: 'enum', values: Object.values(this.config.modelValidate.comment.type.optional), required: false }, - author: { type: 'objectId', required: false }, - article: { type: 'objectId', required: false }, - parent: { type: 'objectId', required: false }, - keyword: { type: 'string', required: false }, - // 时间区间查询仅后台可用,且依赖于createdAt - startDate: { type: 'dateTime', required: false }, - endDate: { type: 'dateTime', required: false }, - // 排序仅后台能用,且order和sortBy需同时传入才起作用 - // -1 desc | 1 asc - order: { type: 'enum', values: [-1, 1], required: false }, - sortBy: { type: 'enum', values: ['createdAt', 'updatedAt', 'ups'], required: false } - }, - create: { - content: { type: 'string', required: true }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, - sticky: { type: 'boolean', required: false }, - type: { type: 'enum', values: Object.values(this.config.modelValidate.comment.type.optional), required: false }, - article: { type: 'objectId', required: false }, - partner: { type: 'objectId', required: false }, - forward: { type: 'objectId', required: false }, - author: { type: 'object', required: true } - }, - update: { - content: { type: 'string', required: false }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, - sticky: { type: 'boolean', required: false } - } - } - } - - async list () { - const { ctx } = this - ctx.query.page = Number(ctx.query.page) - ctx.query.limit = Number(ctx.query.limit) - ctx.validate(this.rules.list, ctx.query) - const { page, limit, state, type, keyword, author, article, parent, order, sortBy, startDate, endDate } = ctx.query - // 过滤条件 - const options = { - sort: { createdAt: 1 }, - page, - limit, - select: '', - populate: [ - { - path: 'author', - select: !ctx._isAuthed ? 'github avatar name site' : '' - }, - { - path: 'parent', - select: 'author meta sticky ups', - match: { - state: 1 - } - }, - { - path: 'forward', - select: 'author meta sticky ups', - match: { - state: 1 - }, - populate: { - path: 'author', - select: 'avatar github name' - } - } - ] - } - - // 查询条件 - const query = {} - if (state !== undefined) { - query.state = state - } - - // 搜索关键词 - if (keyword) { - const keywordReg = new RegExp(keyword) - query.$or = [ - { title: keywordReg } - ] - } - - // 用户 - if (author) { - // 如果是id - if (this.app.utils.validate.isObjectId(author)) { - query.author = author - } else { - // 普通字符串,需要先查到id - const u = await this.service.user.findOne({ name: author }).exec() - query.author = u ? u._id : this.app.utils.share.createObjectId() - } - } - - // 文章 - if (article) { - // 如果是id - if (this.app.utils.validate.isObjectId(article)) { - query.article = article - } else { - // 普通字符串,需要先查到id - const a = await this.service.article.findOne({ name: article }).exec() - query.article = a ? a._id : this.app.utils.share.createObjectId() - } - } - - if (parent) { - // 获取子评论 - query.parent = parent - } else { - // 获取父评论 - query.parent = { $exists: false } - } - - // 未通过权限校验(前台获取文章列表) - if (!ctx._isAuthed) { - // 将评论状态重置为1 - query.state = 1 - query.spam = false - // 评论列表不需要content和state - options.select = '-content -state -updatedAt -spam -type' - } else { - // 排序 - if (sortBy && order) { - options.sort = {} - options.sort[sortBy] = order - } - - // 起始日期 - if (startDate) { - const $gte = new Date(startDate) - if ($gte.toString() !== 'Invalid Date') { - query.createdAt = { $gte } - } - } - - // 结束日期 - if (endDate) { - const $lte = new Date(endDate) - if ($lte.toString() !== 'Invalid Date') { - query.createdAt = Object.assign({}, query.createdAt, { $lte }) - } - } - } - const comments = await this.service.comment.paginate(query, options) - if (!comments) return null - const data = [] - // 查询子评论数量 - await Promise.all(comments.docs.map(doc => { - doc = doc.toObject() - doc.subCount = 0 - data.push(doc) - return this.service.comment.count({ parent: doc._id }).exec() - .then(count => { - doc.subCount = count - }) - })) - comments.docs = data - return this.app.getDocsPaginationData(comments) - } - - async item () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() + async getItemById (id) { let data = null - let queryPs = null - if (!ctx._isAuthed) { - queryPs = this.findOne({ _id: params.id, state: 1, spam: false }) - .select('-content -state -updatedAt -type -spam') - .populate({ - path: 'author', - select: 'github' - }) - .populate({ - path: 'parent', - select: 'author meta sticky ups' - }) - .populate({ - path: 'forward', - select: 'author meta sticky ups' - }) - } else { - queryPs = this.findById(id) - } - - data = await queryPs.populate([ + const populate = [ { path: 'author', - select: 'github' + select: 'github name avatar' }, { path: 'parent', select: 'author meta sticky ups' @@ -213,243 +22,83 @@ module.exports = class CommentService extends ProxyService { path: 'forward', select: 'author meta sticky ups' } - ]).exec() - - return data - } - - async create () { - const { ctx } = this - const body = ctx.validateBody(this.rules.create) - const { article, parent, forward, type, author, content } = body - if (type === this.config.modelValidate.comment.type.optional.COMMENT) { - if (!article) { - return ctx.fail(422, '缺少文章ID') - } - } - if ((parent && !forward) || (!parent && forward)) { - return ctx.fail(422, '缺少parent和forward参数') - } - const user = await this.service.user.checkCommentAuthor(author) - if (!user) { - return ctx.fail('用户不存在') - } else if (user.mute) { - // 被禁言 - return ctx.fail('该用户已被禁言') - } - body.author = user._id - if (!this.service.user.checkUserSpam(user)) { - return ctx.fail('该用户的垃圾评论数量已达到最大限制,已被禁言') - } - const { ip, location } = ctx.getLocation() - body.meta = { - location, - ip, - ua: ctx.req.headers['user-agent'] || '', - referer: ctx.req.headers.referer || '' - } - // 永链 - const permalink = this.getPermalink(body) - const isSpam = await this.app.akismet.checkSpam({ - user_ip: ip, - user_agent: body.meta.ua, - referrer: body.meta.referer, - permalink, - comment_type: getCommentType(type), - comment_author: user.name, - comment_author_email: user.email, - comment_author_url: user.site, - comment_content: content, - is_test: this.app.config.isProd - }) - // 如果是Spam评论 - if (isSpam) { - return ctx.fail('检测为垃圾评论,该评论将不会显示') - } - body.renderedContent = this.app.utils.markdown.render(content) - let data = await this.newAndSave(body) - if (data && data.length) { - data = data[0] - if (!ctx._isAuthed) { - data = await this.findById(data._id) - .select('-content -state -updatedAt') - .populate({ - path: 'author', - select: 'name site avatar role mute email' - }) - .populate({ - path: 'parent', - select: 'author meta sticky ups' - }) - .populate({ - path: 'forward', - select: 'author meta sticky ups' - }) - } else { - data = await this.findById(data._id).exec() - } - ctx.success(data, '评论发布成功') + ] + if (!this.ctx._isAuthed) { + data = await this.getItem( + { _id: id, state: 1, spam: false }, + '-content -state -updatedAt -type -spam', + null, + populate + ) } else { - ctx.fail('评论发布失败') + data = await this.getItem({ _id: id }, null, null, populate) } return data } - async update () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - const body = ctx.validateBody(this.rules.create) - let cache = await this.findById(params.id).populate('author') - if (!cache) { - return ctx.fail('评论不存在') - } - cache = cache.toObject() - if (ctx._isAuthed && ctx._user._id.toString() !== cache.author._id.toString()) { - return ctx.fail('其他人的评论内容不能修改') - } - - if (body.content !== undefined) { - body.renderedContent = this.app.utils.markdown.render(body.content) - } - - // 状态修改是涉及到spam修改 - if (body.state !== undefined) { - const permalink = this.getPermalink(cache) - const opt = { - user_ip: cache.meta.ip, - user_agent: cache.meta.ua, - referrer: cache.meta.referer, - permalink, - comment_type: getCommentType(cache.type), - comment_author: cache.author.github.login, - comment_author_email: cache.author.github.email, - comment_author_url: cache.author.github.blog, - comment_content: cache.content, - is_test: this.config.isProd - } - const SPAM = this.config.modelValidate.comment.state.optional.SPAM - if (cache.state === SPAM && body.state !== SPAM) { - // 垃圾评论转为正常评论 - if (cache.spam) { - body.spam = false - // 报告给Akismet - this.app.akismet.submitSpam(opt) - } - } else if (cache.state !== SPAM && body.state === SPAM) { - // 正常评论转为垃圾评论 - if (!cache.spam) { - body.spam = true - // 报告给Akismet - this.app.akismet.submitHam(opt) - } - } - } - let data = null - if (!ctx._isAuthed) { - data = await this.updateItemById(params.id, body).select('-content -state -updatedAt') - .populate({ - path: 'author', - select: 'github' - }) - .populate({ - path: 'parent', - select: 'author meta sticky ups' - }) - .populate({ - path: 'forward', - select: 'author meta sticky ups' - }) - .exec() - } else { - data = await this.updateItemById(params.id, body).exec() - } - data - ? ctx.success(data, '评论更新成功') - : ctx.fail('评论更新失败') - } - - async delete () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - const data = await this.deleteItemById(params.id).exec() - return data && data.ok && data.n - } - - async like () { - const { ctx } = this - const { params } = ctx - ctx.validateParamsObjectId() - return await this.updateItemById(params.id, { - $inc: { - ups: 1 - } - }) - } - async sendCommentEmailToAdminAndUser (comment) { const { type, article } = comment const commentType = this.config.modelValidate.comment.type.optional const permalink = this.getPermalink(comment) + const adminType = this.getCommentType(comment.type) let adminTitle = '未知的评论' - let adminType = '评论' if (type === commentType.COMMENT) { // 文章评论 - const at = await this.service.article.findById(article).exec().catch(() => null) + const at = await this.service.article.getItemById(article._id || article) if (at && at._id) { - adminTitle = `博客文章 [${at.title}] 有了新的评论` + adminTitle = `博客文章《${at.title}》有了新的评论` } - adminType = '评论' } else if (type === commentType.MESSAGE) { // 站内留言 adminTitle = '个人站点有新的留言' - adminType = '留言' } // 发送给管理员邮箱config.email - this.service.common.sendMail({ + this.service.mail.sendToAdmin({ subject: adminTitle, text: `来自 ${comment.author.name} 的${adminType}:${comment.content}`, - html: `

来自 ${comment.author.name} 的${adminType} [ 点击查看 ]:${comment.renderedContent}

` - }, true) + html: `

来自 ${comment.author.name} 的${adminType} => 点击查看:${comment.renderedContent}

` + }) // 发送给被评论者 if (comment.forward && comment.forward._id !== comment.author._id) { const forwardAuthor = await this.service.user.findById(comment.forward.author).exec().catch(() => null) if (forwardAuthor) { - this.service.comment.sendMail({ + this.service.mail.send({ to: forwardAuthor.github.email, - subject: '你在 Jooger 的博客的评论有了新的回复', + subject: `你在 ${this.config.author} 的博客的评论有了新的回复`, text: `来自 ${comment.author.name} 的回复:${comment.content}`, - html: `

来自 ${comment.author.name} 的回复 [ 点击查看 ]:${comment.renderedContent}

` + html: `

来自 ${comment.author.name} 的回复 => 点击查看:${comment.renderedContent}

` }) } } } + /** + * @desc 获取评论所属页面链接 + * @param {Comment} comment 评论 + * @return {String} 页面链接 + */ getPermalink (comment = {}) { const { type, article } = comment - const commentType = this.config.modelValidate.comment.type.optional + const { COMMENT, MESSAGE } = this.config.modelValidate.comment.type.optional + const url = this.config.author.url switch (type) { - case commentType.COMMENT: - return `${this.config.author.url}/articles/${article}` - case commentType.MESSAGE: - return `${this.config.author.url}/guestbook` + case COMMENT: + return `${url}/articles/${article._id || article}` + case MESSAGE: + return `${url}/guestbook` default: return '' } } -} -// 评论类型说明 -function getCommentType (type) { - switch (type) { - case 0: - return '文章评论' - case 1: - return '站点留言' - default: - return '评论' + /** + * @desc 获取评论类型文案 + * @param {Number | String} type 评论类型 + * @return {String} 文案 + */ + getCommentType (type) { + return ['文章评论', '站点留言'][type] || '评论' } } diff --git a/app/service/common.js b/app/service/common.js deleted file mode 100644 index 659f723..0000000 --- a/app/service/common.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * @desc Util Services - */ - -const { Service } = require('egg') -const axios = require('axios') - -const prefix = 'http://' -let mailerClient = null - -module.exports = class UtilService extends Service { - proxyUrl (url) { - if (url.startsWith(prefix)) { - return url.replace(prefix, `${this.app.config.author.url}/proxy/`) - } - return url - } - - async getGithubUserInfo (username) { - if (!username) return null - let gayhub = {} - if (this.config.isLocal) { - gayhub = this.config.github - } else { - const keys = this.service.setting.keys() - if (!keys || !keys.github) { - this.logger.warn('未找到gayhub配置') - return null - } - gayhub = keys.github - } - const { clientID, clientSecret } = gayhub - try { - const res = await axios.get(`https://api.github.com/users/${username}`, { - params: { - client_id: clientID, - client_secret: clientSecret - } - }, { - headers: { - Accept: 'application/json' - } - }) - if (res && res.status === 200) { - this.logger.info(`【 ${username} 】信息抓取成功`) - return res.data - } - return null - } catch (error) { - this.logger.warn(`【 ${username} 】信息抓取失败`) - this.logger.error(error) - return null - } - } - - async getGithubUsersInfo (usernames = '') { - if (!usernames) { - return null - } else if (this.app.utils.validate.isString(usernames)) { - usernames = [usernames] - } else if (!Array.isArray(usernames)) { - return null - } - return await Promise.all(usernames.map(name => this.getGithubUserInfo(name))) - } - - async getGithubAuthUserInfo (access_token) { - return await axios.get('https://api.github.com/user', { - params: { access_token } - }) - } - - async generateLinks (links = []) { - if (links && links.length) { - const githubNames = links.map(link => link.github) - const usersInfo = await this.getGithubUsersInfo(githubNames) - if (usersInfo) { - return links.map((link, index) => { - const userInfo = usersInfo[index] - if (userInfo) { - link.avatar = this.proxyUrl(userInfo.avatar_url) - link.slogan = userInfo.bio - link.site = link.site || userInfo.blog || userInfo.url - } - return link - }) - } - } - return links - } - - // 发送邮件 - async sendMail (data, toMe = false) { - let client = mailerClient - const keys = await this.service.setting.keys() - if (!client) { - mailerClient = client = this.app.mailer.getClient({ - auth: keys.mail - }) - await this.app.mailer.verify() - } - return new Promise((resolve, reject) => { - const opt = Object.assign({ - from: `${this.config.author.name} <${keys.mail.user}>` - }, data) - if (toMe) { - opt.to = keys.mail.user - } - client.sendMail(opt, (err, info) => { - if (err) { - this.logger.error('邮件发送失败,' + err.message) - return reject(err) - } - this.logger.info('邮件发送成功') - resolve(info) - }) - }) - } -} diff --git a/app/service/mail.js b/app/service/mail.js new file mode 100644 index 0000000..55fc0b3 --- /dev/null +++ b/app/service/mail.js @@ -0,0 +1,41 @@ +/** + * @desc Mail Services + */ + +const { Service } = require('egg') + +let mailerClient = null + +module.exports = class MailService extends Service { + // 发送邮件 + async send (data, toAdmin = false) { + let client = mailerClient + const keys = this.app.setting.keys + if (!client) { + mailerClient = client = this.app.mailer.getClient({ + auth: keys.mail + }) + await this.app.mailer.verify() + } + const opt = Object.assign({ + from: `${this.config.author.name} <${keys.mail.user}>` + }, data) + if (toAdmin) { + opt.to = keys.mail.user + } + await new Promise((resolve, reject) => { + client.sendMail(opt, (err, info) => { + if (err) { + this.logger.error('邮件发送失败,' + err.message) + return reject(err) + } + this.logger.info('邮件发送成功,TO:' + opt.to) + resolve(info) + }) + }) + } + + sendToAdmin (data) { + return this.send(data, true) + } +} diff --git a/app/service/proxy.js b/app/service/proxy.js index 5690503..c9b84f1 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -9,62 +9,77 @@ module.exports = class ProxyService extends Service { return this.model.init() } - newAndSave (docs) { - if (!Array.isArray(docs)) { - docs = [docs] + getList (query, select = null, opt, populate = []) { + const Q = this.model.find(query, select, opt) + if (populate) { + [].concat(populate).forEach(item => Q.populate(item)) } - return this.model.create(docs) + return Q.exec() } - paginate (query, opt = {}) { - return this.model.paginate(query, opt) + async getLimitListByQuery (query, opt) { + opt = Object.assign({ lean: true }, opt) + const data = await this.model.paginate(query, opt) + return this.app.getDocsPaginationData(data) } - findById (id, select = null, opt = {}) { - return this.model.findById(id, select, opt) - } - - find (query = {}, select = null, opt = {}) { - return this.model.find(query, select, opt) - } - - findOne (query = {}, select = null, opt = {}) { - return this.model.findOne(query, select, opt) - } - - updateItemById (id, doc, opt = {}) { - return this.model.findByIdAndUpdate(id, doc, Object.assign({ new: true }, opt)) - } - - updateOne (query = {}, doc = {}, opt = {}) { - return this.model.findOneAndUpdate(query, doc, Object.assign({ new: true }, opt)) + getItem (query, select = null, opt, populate = []) { + opt = this.app.merge({ + lean: true + }, opt) + let Q = this.model.findOne(query, select, opt) + if (populate) { + [].concat(populate).forEach(item => { + Q = Q.populate(item) + }) + } + return Q.exec() } - updateMany (query = {}, doc = {}, opt = {}) { - return this.model.update(query, doc, Object.assign({ multi: true }, opt)) + getItemById (id, select = null, opt, populate = []) { + opt = this.app.merge({ + lean: true + }, opt) + const Q = this.model.findById(id, select, opt) + if (populate) { + [].concat(populate).forEach(item => Q.populate(item)) + } + return Q.exec() } - remove (query = {}) { - return this.model.remove(query) + create (data) { + return this.model.create(data) } - deleteItemById (id = '') { - return this.model.deleteOne({ _id: id }) + updateItem (query = {}, data, opt, populate = []) { + opt = this.app.merge({ + lean: true, + new: true + }) + const Q = this.model.findOneAndUpdate(query, data, opt) + if (populate) { + [].concat(populate).forEach(item => Q.populate(item)) + } + return Q.exec() } - deleteItemByIds (ids = []) { - return this.model.deleteMany({ - _id: { - $in: ids - } + updateItemById (id, data, opt, populate = []) { + opt = this.app.merge({ + lean: true, + new: true }) + const Q = this.model.findByIdAndUpdate(id, data, opt) + if (populate) { + [].concat(populate).forEach(item => Q.populate(item)) + } + return Q.exec() } - aggregate (opt = {}) { - return this.model.aggregate(opt) + deleteItemById (id, opt) { + return this.model.findByIdAndDelete(id, opt).exec() } - count (query = {}) { - return this.model.count(query) + aggregate (pipeline = []) { + return this.model.aggregate(pipeline) } } diff --git a/app/service/proxy2.js b/app/service/proxy2.js deleted file mode 100644 index 56d4c0a..0000000 --- a/app/service/proxy2.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @desc 公共的model proxy service - */ - -const { Service } = require('egg') - -module.exports = class ProxyService extends Service { - init () { - return this.model.init() - } - - getList (query, select = null, opt, populate = []) { - const Q = this.model.find(query, select, opt) - if (populate) { - [].concat(populate).forEach(item => Q.populate(item)) - } - return Q.exec() - } - - getItem (query, select = null, opt, populate = []) { - opt = this.app.merge({ - lean: true - }, opt) - const Q = this.model.findOne(query, select, opt) - if (populate) { - [].concat(populate).forEach(item => Q.populate(item)) - } - return Q.exec() - } - - getItemById (id, select = null, opt, populate = []) { - opt = this.app.merge({ - lean: true - }, opt) - const Q = this.model.findById(id, select, opt) - if (populate) { - [].concat(populate).forEach(item => Q.populate(item)) - } - return Q.exec() - } - - create (data) { - return new this.model(data).save() - } - - updateItem (query = {}, data, opt, populate = []) { - opt = this.app.merge({ - lean: true, - new: true - }) - const Q = this.model.findOneAndUpdate(query, data, opt) - if (populate) { - [].concat(populate).forEach(item => Q.populate(item)) - } - return Q.exec() - } - - updateItemById (id, data, opt, populate = []) { - opt = this.app.merge({ - lean: true, - new: true - }) - const Q = this.model.findByIdAndUpdate(id, data, opt) - if (populate) { - [].concat(populate).forEach(item => Q.populate(item)) - } - return Q.exec() - } - - deleteItemById (id, opt) { - return this.model.findByIdAndDelete(id, opt).exec() - } - - aggregate (pipeline = []) { - return this.model.aggregate(pipeline) - } -} diff --git a/app/service/setting.js b/app/service/setting.js index 1a945fa..9c4512a 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -2,7 +2,7 @@ * @desc Setting Services */ -const ProxyService = require('./proxy2') +const ProxyService = require('./proxy') module.exports = class SettingService extends ProxyService { get model () { @@ -69,6 +69,9 @@ module.exports = class SettingService extends ProxyService { return setting } + /** + * @desc 把配置挂载到app上 + */ async mountToApp () { const setting = await this.getItem() this.app.setting = setting || null diff --git a/app/service/tag.js b/app/service/tag.js index e815039..7ff9385 100644 --- a/app/service/tag.js +++ b/app/service/tag.js @@ -2,7 +2,7 @@ * @desc 标签 Services */ -const ProxyService = require('./proxy2') +const ProxyService = require('./proxy') module.exports = class TagService extends ProxyService { get model () { @@ -28,15 +28,4 @@ module.exports = class TagService extends ProxyService { } return categories } - - async getItem (id, select = null, opt) { - opt = this.app.merge({ - lean: true - }, opt) - const category = await this.model.findById(id, select, opt).exec() - if (category) { - category.articles = await this.service.article.getList({ category: category._id }) - } - return category - } } diff --git a/app/service/user.js b/app/service/user.js index fb343b2..e4c2d36 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -2,7 +2,7 @@ * @desc User Services */ -const ProxyService = require('./proxy2') +const ProxyService = require('./proxy') module.exports = class UserService extends ProxyService { get model () { @@ -26,25 +26,36 @@ module.exports = class UserService extends ProxyService { return data } + /** + * @desc 评论用户创建或更新 + * @param {*} author 评论的author + * @return {User} user + */ async checkCommentAuthor (author) { let user = null const { isObjectId, isObject } = this.app.utils.validate if (isObjectId(author)) { - user = this.findById(author).select('-password').exec() + user = await this.getItemById(author) } else if (isObject(author)) { const update = {} author.name && (update.name = author.name) author.site && (update.site = author.site) + update.avatar = this.app.utils.gravatar(author.email) if (author.email) { - update.avatar = this.app.utils.gravatar(author.email) update.email = author.email } - if (author.id) { + const id = author.id || author._id + if (id) { // 更新 - if (isObjectId(author.id)) { - user = await this.updateItemById(author.id, update).exec() - if (user) { - this.logger.info('用户更新成功:' + user.name) + if (isObjectId(id)) { + user = await this.getItemById(id) + const hasDiff = user && Object.keys(update).some(key => update[key] !== user[key]) + if (hasDiff) { + // 有变动才更新 + user = await this.updateItemById(id, update) + if (user) { + this.logger.info('用户更新成功:' + user.name) + } } } } else { @@ -57,14 +68,20 @@ module.exports = class UserService extends ProxyService { return user } - // 检测用户以往spam评论 + /** + * @desc 检测用户以往spam评论 + * @param {User} user 评论作者 + * @return {Boolean} 是否能发布评论 + */ async checkUserSpam (user) { - const comments = await this.service.comment.find({ author: user._id }).exec() + if (!user) return + const comments = await this.service.comment.getList({ author: user._id }) const spams = comments.filter(c => c.spam) - if (spams.length >= this.config.limit.commentSpamLimit) { + if (spams.length >= this.app.setting.limit.commentSpamMaxCount) { + // 如果已存在垃圾评论数达到最大限制 if (!user.mute) { - await this.updateItemById(user._id, { mute: true }).exec() - this.logger.info(`用户【${user.name}】禁言成功`) + user = await this.updateItemById(user._id, { mute: true }) + this.logger.info(`用户禁言成功:${user.name}`) } return false } diff --git a/config/config.default.js b/config/config.default.js index 6d9e7f4..409896b 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -151,7 +151,7 @@ module.exports = appInfo => { password: 'admin123456' } - config.defaultAvatar = 'http://static.jooger.me/img/common/default-avatar.png' + config.defaultAvatar = 'https://static.jooger.me/img/common/avatar.png' // 限制参数 config.limit = { From 4a33dfefa86357817f1afad90d948f16a77121c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Thu, 30 Aug 2018 01:07:33 +0800 Subject: [PATCH 124/208] update: detail optimize --- app.js | 10 +++++-- app/controller/article.js | 50 ++++++++++++------------------- app/controller/auth.js | 29 +++++++++++++----- app/controller/category.js | 13 +++++++-- app/controller/comment.js | 60 ++++++++++++-------------------------- app/controller/setting.js | 10 +++++-- app/controller/tag.js | 13 +++++++-- app/controller/user.js | 4 +-- app/extend/context.js | 26 ++++++++++++++--- app/middleware/auth.js | 10 ++++--- app/model/article.js | 2 +- app/model/user.js | 2 +- app/router/backend.js | 9 ++---- app/router/frontend.js | 2 +- app/service/article.js | 6 ++-- app/service/auth.js | 6 ++++ app/service/comment.js | 5 +++- app/utils/validate.js | 2 +- config/config.default.js | 5 ++-- 19 files changed, 147 insertions(+), 117 deletions(-) diff --git a/app.js b/app.js index bc4f667..8f64112 100644 --- a/app.js +++ b/app.js @@ -22,10 +22,16 @@ function addValidateRule (app) { } }) app.validator.addRule('email', (rule, val) => { - return app.utils.validate.isEmail(val) + const valid = app.utils.validate.isEmail(val) + if (!valid) { + return 'must be email' + } }) app.validator.addRule('url', (rule, val) => { - return app.utils.validate.isSiteUrl(val) + const valid = app.utils.validate.isUrl(val) + if (!valid) { + return 'must be url' + } }) } diff --git a/app/controller/article.js b/app/controller/article.js index ba7e8a6..371ef8d 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -8,8 +8,8 @@ module.exports = class ArticleController extends Controller { get rules () { return { list: { - page: { type: 'number', required: true, min: 1 }, - limit: { type: 'number', required: false, min: 1 }, + page: { type: 'int', required: true, min: 1 }, + limit: { type: 'int', required: false, min: 1 }, state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, category: { type: 'objectId', required: false }, tag: { type: 'objectId', required: false }, @@ -75,10 +75,7 @@ module.exports = class ArticleController extends Controller { } ] } - const query = {} - if (state !== undefined) { - query.state = state - } + const query = { state, category, tag } // 搜索关键词 if (keyword) { @@ -88,32 +85,8 @@ module.exports = class ArticleController extends Controller { ] } - // 分类 - if (category) { - // 如果是id - if (this.app.utils.validate.isObjectId(category)) { - query.category = category - } else { - // 普通字符串,需要先查到id - const c = await this.service.category.getItem({ name: category }) - query.category = c ? c._id : this.app.utils.share.createObjectId() - } - } - - // 标签 - if (tag) { - // 如果是id - if (this.app.utils.validate.isObjectId(tag)) { - query.tag = tag - } else { - // 普通字符串,需要先查到id - const t = await this.service.tag.getItem({ name: tag }) - query.tag = t ? t._id : this.app.utils.share.createObjectId() - } - } - // 未通过权限校验(前台获取文章列表) - if (!ctx._isAuthed) { + if (!ctx.session._isAuthed) { // 将文章状态重置为1 query.state = 1 // 文章列表不需要content和state @@ -141,7 +114,7 @@ module.exports = class ArticleController extends Controller { } } } - const data = await this.service.article.getLimitListByQuery(query, options) + const data = await this.service.article.getLimitListByQuery(ctx.processPayload(query), options) data ? ctx.success(data, '文章列表获取成功') : ctx.fail('文章列表获取失败') @@ -162,6 +135,10 @@ module.exports = class ArticleController extends Controller { if (body.createdAt) { body.createdAt = new Date(body.createdAt) } + const exist = await this.service.article.getItem({ title: body.title }) + if (exist) { + return ctx.fail('文章名称重复') + } const data = await this.service.article.create(body) data ? ctx.success(data, '文章创建成功') @@ -175,6 +152,15 @@ module.exports = class ArticleController extends Controller { if (body.createdAt) { body.createdAt = new Date(body.createdAt) } + const exist = await this.service.article.getItem({ + _id: { + $ne: params.id + }, + title: body.title + }) + if (exist) { + return ctx.fail('文章名称重复') + } const data = await this.service.article.updateItemById( params.id, body, diff --git a/app/controller/auth.js b/app/controller/auth.js index e6fde15..9537522 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -19,7 +19,7 @@ module.exports = class AuthController extends Controller { site: { type: 'url', required: false }, description: { type: 'string', required: false }, avatar: { type: 'string', required: false }, - slogan: { type: 'string', required: false }, + l: { type: 'string', required: false }, company: { type: 'string', required: false }, location: { type: 'string', required: false } }, @@ -48,22 +48,34 @@ module.exports = class AuthController extends Controller { async logout () { const { ctx } = this - this.service.auth.setCookie(ctx._user, false) - this.logger.info(`用户退出成功, 用户ID:${ctx._user._id},用户名:${ctx._user.name}`) + this.service.auth.setCookie(ctx.session._user, false) + this.logger.info(`用户退出成功, 用户ID:${ctx.session._user._id},用户名:${ctx.session._user.name}`) ctx.success('退出成功') } async info () { - this.ctx.success(this.ctx._user, '管理员信息获取成功') + this.ctx.success(this.ctx.session._user, '管理员信息获取成功') } /** * @desc 管理员信息更新,不包含密码更新 + * @return {*} null */ async update () { const { ctx } = this const body = this.ctx.validateBody(this.rules.update) - const data = await this.service.user.updateItemById(ctx._user_id, body) + const exist = await this.service.user.getItemById(ctx.session._user._id) + if (exist && exist.name !== body.name) { + // 检测变更的name是否和其他用户冲突 + const conflict = await this.service.user.getItem({ name: ctx.session._user.name }) + if (conflict) { + // 有冲突 + return ctx.fail('用户名重复') + } + } + const data = await this.service.user.updateItemById(ctx.session._user._id, body) + // 更新session + await this.service.auth.updateSessionUser() data ? ctx.success(data, '管理员信息更新成功') : ctx.fail('管理员信息更新失败') @@ -75,15 +87,16 @@ module.exports = class AuthController extends Controller { async password () { const { ctx } = this const body = this.ctx.validateBody(this.rules.password) - const vertifyPassword = this.app.utils.encode.bcompare(body.oldPassword, ctx._user.password) + const exist = await this.service.user.getItemById(ctx.session._user._id) + const vertifyPassword = this.app.utils.encode.bcompare(body.oldPassword, exist.password) if (!vertifyPassword) { ctx.throw(200, '原密码错误') } - const data = await this.service.user.updateItemById(ctx._user._id, { + const data = await this.service.user.updateItemById(ctx.session._user._id, { password: this.app.utils.encode.bhash(body.password) }) data - ? ctx.success(data, '密码更新成功') + ? ctx.success('密码更新成功') : ctx.fail('密码更新失败') } } diff --git a/app/controller/category.js b/app/controller/category.js index dc4b05b..2918951 100644 --- a/app/controller/category.js +++ b/app/controller/category.js @@ -75,7 +75,7 @@ module.exports = class CategoryController extends Controller { const { name } = body const exist = await this.service.category.getItem({ name }) if (exist) { - return ctx.fail('分类已存在') + return ctx.fail('分类名称重复') } const data = await this.service.category.create(body) data @@ -87,6 +87,15 @@ module.exports = class CategoryController extends Controller { const { ctx } = this const params = ctx.validateParamsObjectId() const body = ctx.validateBody(this.rules.update) + const exist = await this.service.category.getItem({ + name: body.name, + _id: { + $nin: [ params.id ] + } + }) + if (exist) { + return ctx.fail('分类名称重复') + } const data = await this.service.category.updateItemById(params.id, body) data ? ctx.success(data, '分类更新成功') @@ -96,7 +105,7 @@ module.exports = class CategoryController extends Controller { async delete () { const { ctx } = this const params = ctx.validateParamsObjectId() - const articles = await this.service.article.getList({ category: params._id }, 'title') + const articles = await this.service.article.getList({ category: params.id }, 'title state createdAt') if (articles.length) { return ctx.fail('该分类下还有文章,不能删除', articles) } diff --git a/app/controller/comment.js b/app/controller/comment.js index 31eec32..5d5365f 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -8,8 +8,8 @@ module.exports = class CommentController extends Controller { get rules () { return { list: { - page: { type: 'number', required: true, min: 1 }, - limit: { type: 'number', required: false, min: 1 }, + page: { type: 'int', required: true, min: 1 }, + limit: { type: 'int', required: false, min: 1 }, state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, type: { type: 'enum', values: Object.values(this.config.modelValidate.comment.type.optional), required: false }, author: { type: 'objectId', required: false }, @@ -46,29 +46,31 @@ module.exports = class CommentController extends Controller { ctx.query.limit = Number(ctx.query.limit) } ctx.validate(this.rules.list, ctx.query) - const { page, limit, state, keyword, author, article, parent, order, sortBy, startDate, endDate } = ctx.query + const { page, limit, state, type, keyword, author, article, parent, order, sortBy, startDate, endDate } = ctx.query // 过滤条件 const options = { - sort: { createdAt: 1 }, + sort: { + createdAt: 1 + }, page, limit: limit || this.app.setting.limit.commentCount, select: '', populate: [ { path: 'author', - select: !ctx._isAuthed ? 'github avatar name site' : '' + select: !ctx.session._isAuthed ? 'github avatar name site' : '-password' }, { path: 'parent', select: 'author meta sticky ups', - match: !ctx._isAuthed && { + match: !ctx.session._isAuthed && { state: 1 } || null }, { path: 'forward', select: 'author meta sticky ups', - match: !ctx._isAuthed && { + match: !ctx.session._isAuthed && { state: 1 } || null, populate: { @@ -80,10 +82,7 @@ module.exports = class CommentController extends Controller { } // 查询条件 - const query = {} - if (state !== undefined) { - query.state = state - } + const query = { state, type, author, article } // 搜索关键词 if (keyword) { @@ -93,30 +92,6 @@ module.exports = class CommentController extends Controller { ] } - // 用户 - if (author) { - // 如果是id - if (this.app.utils.validate.isObjectId(author)) { - query.author = author - } else { - // 普通字符串,需要先查到id - const u = await this.service.user.getItem({ name: author }) - query.author = u ? u._id : this.app.utils.share.createObjectId() - } - } - - // 文章 - if (article) { - // 如果是id - if (this.app.utils.validate.isObjectId(article)) { - query.article = article - } else { - // 普通字符串,需要先查到id - const a = await this.service.article.getItem({ name: article }) - query.article = a ? a._id : this.app.utils.share.createObjectId() - } - } - if (parent) { // 获取子评论 query.parent = parent @@ -126,7 +101,7 @@ module.exports = class CommentController extends Controller { } // 未通过权限校验(前台获取文章列表) - if (!ctx._isAuthed) { + if (!ctx.session._isAuthed) { // 将评论状态重置为1 query.state = 1 query.spam = false @@ -155,7 +130,7 @@ module.exports = class CommentController extends Controller { } } } - const data = await this.service.comment.getLimitListByQuery(query, options) + const data = await this.service.comment.getLimitListByQuery(ctx.processPayload(query), options) data ? ctx.success(data, '评论列表获取成功') : ctx.fail('评论列表获取失败') @@ -174,16 +149,19 @@ module.exports = class CommentController extends Controller { const { ctx } = this ctx.validateCommentAuthor() const body = ctx.validateBody(this.rules.create) - const COMMENT = this.config.modelValidate.comment.type.optional.COMMENT + const { COMMENT, MESSAGE } = type === this.config.modelValidate.comment.optional body.author = ctx.request.body.author const { article, parent, forward, type, content, author } = body if (type === COMMENT) { if (!article) { return ctx.fail(422, '缺少文章ID') } + } else if (type === MESSAGE) { + // 站内留言 + delete body.article } if ((parent && !forward) || (!parent && forward)) { - return ctx.fail(422, '缺少parent和forward参数') + return ctx.fail(422, '缺少父评论ID或被回复评论ID') } const user = await this.service.user.checkCommentAuthor(author) if (!user) { @@ -250,7 +228,7 @@ module.exports = class CommentController extends Controller { if (!exist) { return ctx.fail('评论不存在') } - if (ctx._isAuthed && ctx._user._id !== exist.author._id) { + if (ctx.session._isAuthed && ctx.session._user._id !== exist.author._id) { return ctx.fail('非本人评论不能修改') } // 状态修改是涉及到spam修改 @@ -287,7 +265,7 @@ module.exports = class CommentController extends Controller { } body.renderedContent = this.app.utils.markdown.render(body.content) let data = null - if (!ctx._isAuthed) { + if (!ctx.session._isAuthed) { data = await this.service.comment.updateItemById( params.id, body, diff --git a/app/controller/setting.js b/app/controller/setting.js index 4a207a4..57b0a62 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -12,11 +12,13 @@ module.exports = class SettingController extends Controller { }, create: { site: { type: 'object', required: false }, - keys: { type: 'object', required: false } + keys: { type: 'object', required: false }, + limit: { type: 'object', required: false } }, update: { site: { type: 'object', required: false }, - keys: { type: 'object', required: false } + keys: { type: 'object', required: false }, + limit: { type: 'object', required: false } } } } @@ -37,7 +39,9 @@ module.exports = class SettingController extends Controller { if (!exist) { return ctx.fail('配置未找到') } - body = this.app.merge(exist, body) + body = this.app.merge({}, exist, body) + console.log(body); + await this.service.setting.updateItemById(exist._id, body) // 抓取友链 const data = await this.service.setting.updateLinks() diff --git a/app/controller/tag.js b/app/controller/tag.js index 6205348..980c5f3 100644 --- a/app/controller/tag.js +++ b/app/controller/tag.js @@ -75,7 +75,7 @@ module.exports = class TagController extends Controller { const { name } = body const exist = await this.service.tag.getItem({ name }) if (exist) { - return ctx.fail('标签已存在') + return ctx.fail('标签名称重复') } const data = await this.service.tag.create(body) data @@ -87,6 +87,15 @@ module.exports = class TagController extends Controller { const { ctx } = this const params = ctx.validateParamsObjectId() const body = ctx.validateBody(this.rules.update) + const exist = await this.service.tag.getItem({ + name: body.name, + _id: { + $nin: [ params.id ] + } + }) + if (exist) { + return ctx.fail('标签名称重复') + } const data = await this.service.tag.updateItemById(params.id, body) data ? ctx.success(data, '标签更新成功') @@ -96,7 +105,7 @@ module.exports = class TagController extends Controller { async delete () { const { ctx } = this const params = ctx.validateParamsObjectId() - const articles = await this.service.article.getList({ tag: params._id }, 'title') + const articles = await this.service.article.getList({ tag: params.id }, 'title') if (articles.length) { return ctx.fail('该标签下还有文章,不能删除', articles) } diff --git a/app/controller/user.js b/app/controller/user.js index 5dbe356..23ae17f 100644 --- a/app/controller/user.js +++ b/app/controller/user.js @@ -8,7 +8,7 @@ module.exports = class UserController extends Controller { async list () { const { ctx } = this let select = '-password' - if (!ctx._isAuthed) { + if (!ctx.session._isAuthed) { select += ' -createdAt -updatedAt -role' } const data = await this.service.user.getList({}, select) @@ -21,7 +21,7 @@ module.exports = class UserController extends Controller { const { ctx } = this const { id } = ctx.validateParamsObjectId() let select = '-password' - if (!ctx._isAuthed) { + if (!ctx.session._isAuthed) { select += ' -createdAt -updatedAt -github' } const data = await this.service.user.getItemById(id, select) diff --git a/app/extend/context.js b/app/extend/context.js index a045986..078f89e 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -1,19 +1,37 @@ const geoip = require('geoip-lite') module.exports = { + processPayload (payload) { + if (!payload) return null + const result = {} + for (const key in payload) { + if (payload.hasOwnProperty(key)) { + const value = payload[key] + if (value !== undefined) { + result[key] = value + } + } + } + return result + }, validateParams (rules) { this.validate(rules, this.params) return this.params }, - validateBody (rules, body) { - body = body || this.request.body + validateBody (rules, body, dry = true) { + if (typeof body === 'number') { + dry = body + body = this.request.body + } else { + body = body || this.request.body + } this.validate(rules, body) - return Object.keys(rules).reduce((res, key) => { + return dry && Object.keys(rules).reduce((res, key) => { if (body.hasOwnProperty(key)) { res[key] = body[key] } return res - }, {}) + }, {}) || body }, validateParamsObjectId () { return this.validateParams({ diff --git a/app/middleware/auth.js b/app/middleware/auth.js index 9336246..4b1e81a 100644 --- a/app/middleware/auth.js +++ b/app/middleware/auth.js @@ -19,9 +19,9 @@ module.exports = app => { if (!user) { return ctx.fail(401, '用户不存在') } - ctx._user = user - ctx._isAdmin = user.role === app.config.modelValidate.user.role.optional.ADMIN - ctx._isAuthed = true + ctx.session._user = user + ctx.session._isAdmin = user.role === app.config.modelValidate.user.role.optional.ADMIN + ctx.session._isAuthed = true await next() } ]) @@ -39,7 +39,7 @@ function verifyToken (app) { decodedToken = await jwt.verify(token, config.secrets) } catch (err) { logger.warn('Token校验出错,错误:' + err.message) - return ctx.fail(401, '登录失效') + return ctx.fail(401, '登录失效,请重新登录') } if (decodedToken && decodedToken.exp > Math.floor(Date.now() / 1000)) { // 已校验权限 @@ -47,6 +47,8 @@ function verifyToken (app) { ctx.session._token = token logger.info('Token校验成功') } + } else { + return ctx.fail('请先登录') } await next() } diff --git a/app/model/article.js b/app/model/article.js index 279ad8d..350cf8d 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -23,7 +23,7 @@ module.exports = app => { // 标签 tag: [{ type: Schema.Types.ObjectId, ref: 'Tag' }], // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) - thumb: { type: String, validate: /.+?\.(jpg|jpeg|gif|bmp|png)/ }, + thumb: { type: String, validate: app.utils.validate.isUrl }, // 文章状态 ( 0 草稿 | 1 已发布 ) state: { type: String, diff --git a/app/model/user.js b/app/model/user.js index 9179ad2..19f4446 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -11,7 +11,7 @@ module.exports = app => { name: { type: String, required: true }, email: { type: String, required: true, validate: app.utils.validate.isEmail }, avatar: { type: String, required: true }, - site: { type: String, validate: app.utils.validate.isSiteUrl }, + site: { type: String, validate: app.utils.validate.isUrl }, slogan: { type: String }, description: { type: String, default: '' }, // 角色 0 管理员 | 1 普通用户 diff --git a/app/router/backend.js b/app/router/backend.js index a3a99d0..b0c6df9 100644 --- a/app/router/backend.js +++ b/app/router/backend.js @@ -8,7 +8,6 @@ module.exports = app => { backendRouter.get('/articles/archives', auth, controller.article.archives) backendRouter.get('/articles/:id', auth, controller.article.item) backendRouter.post('/articles', auth, controller.article.create) - backendRouter.put('/articles/:id', auth, controller.article.update) backendRouter.patch('/articles/:id', auth, controller.article.update) backendRouter.patch('/articles/:id/like', auth, controller.article.like) backendRouter.delete('/articles/:id', auth, controller.article.delete) @@ -17,7 +16,6 @@ module.exports = app => { backendRouter.get('/categories', auth, controller.category.list) backendRouter.get('/categories/:id', auth, controller.category.item) backendRouter.post('/categories', auth, controller.category.create) - backendRouter.put('/categories/:id', auth, controller.category.update) backendRouter.patch('/categories/:id', auth, controller.category.update) backendRouter.delete('/categories/:id', auth, controller.category.delete) @@ -25,7 +23,6 @@ module.exports = app => { backendRouter.get('/tags', auth, controller.tag.list) backendRouter.get('/tags/:id', auth, controller.tag.item) backendRouter.post('/tags', auth, controller.tag.create) - backendRouter.put('/tags/:id', auth, controller.tag.update) backendRouter.patch('/tags/:id', auth, controller.tag.update) backendRouter.delete('/tags/:id', auth, controller.tag.delete) @@ -35,7 +32,7 @@ module.exports = app => { backendRouter.post('/comments', auth, controller.comment.create) backendRouter.patch('/comments/:id', auth, controller.comment.update) backendRouter.delete('/comments/:id', auth, controller.comment.delete) - backendRouter.post('/comments/:id/like', auth, controller.comment.like) + backendRouter.patch('/comments/:id/like', auth, controller.comment.like) // User backendRouter.get('/users', auth, controller.user.list) @@ -43,12 +40,12 @@ module.exports = app => { // Setting backendRouter.get('/setting', auth, controller.setting.index) - backendRouter.put('/setting', auth, controller.setting.update) backendRouter.patch('/setting', auth, controller.setting.update) // Auth backendRouter.post('/auth/login', controller.auth.login) backendRouter.get('/auth/logout', auth, controller.auth.logout) backendRouter.get('/auth/info', auth, controller.auth.info) - backendRouter.post('/auth/password', auth, controller.auth.password) + backendRouter.patch('/auth/info', auth, controller.auth.update) + backendRouter.patch('/auth/password', auth, controller.auth.password) } diff --git a/app/router/frontend.js b/app/router/frontend.js index 323448b..cff8d19 100644 --- a/app/router/frontend.js +++ b/app/router/frontend.js @@ -20,7 +20,7 @@ module.exports = app => { fontendRouter.get('/comments', controller.comment.list) fontendRouter.get('/comments/:id', controller.comment.item) fontendRouter.post('/comments', controller.comment.create) - fontendRouter.post('/comments/:id/like', controller.comment.like) + fontendRouter.patch('/comments/:id/like', controller.comment.like) // User fontendRouter.get('/users/:id', controller.user.item) diff --git a/app/service/article.js b/app/service/article.js index ccaa437..8e4f390 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -12,7 +12,7 @@ module.exports = class ArticleService extends ProxyService { async getItemById (id, select, opt = {}, single = false) { let api = this.getItem.bind(this) const query = { _id: id } - if (!this.ctx._isAuthed) { + if (!this.ctx.session._isAuthed) { api = this.updateItem.bind(this) // 前台博客访问文章的时候pv+1 query.state = this.config.modelValidate.article.state.optional.PUBLISH @@ -40,7 +40,7 @@ module.exports = class ArticleService extends ProxyService { title: 1, createdAt: 1 } - if (!this.ctx._isAuthed) { + if (!this.ctx.session._isAuthed) { $match.state = 1 } else { $project.state = 1 @@ -125,7 +125,7 @@ module.exports = class ArticleService extends ProxyService { } } // 如果未通过权限校验,将文章状态重置为1 - if (!this.ctx._isAuthed) { + if (!this.ctx.session._isAuthed) { query.state = this.config.modelValidate.article.state.optional.PUBLISH } const prev = await this.getItem( diff --git a/app/service/auth.js b/app/service/auth.js index 3f1c9ef..6e6e533 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -50,4 +50,10 @@ module.exports = class AuthService extends Service { } } } + + // 更新session + async updateSessionUser (admin) { + this.ctx.session._user = admin || await this.service.user.getItemById(this.ctx.session._user._id, '-password') + this.logger.info('Session管理员信息更新成功') + } } diff --git a/app/service/comment.js b/app/service/comment.js index 89b2106..ffa8cd7 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -21,9 +21,12 @@ module.exports = class CommentService extends ProxyService { }, { path: 'forward', select: 'author meta sticky ups' + }, { + path: 'article', + select: 'title, description thumb createdAt' } ] - if (!this.ctx._isAuthed) { + if (!this.ctx.session._isAuthed) { data = await this.getItem( { _id: id, state: 1, spam: false }, '-content -state -updatedAt -type -spam', diff --git a/app/utils/validate.js b/app/utils/validate.js index ecd2091..a76bbe3 100644 --- a/app/utils/validate.js +++ b/app/utils/validate.js @@ -16,7 +16,7 @@ Object.keys(validator).forEach(key => { } }) -exports.isSiteUrl = (site = '') => validator.isURL(site, { +exports.isUrl = (site = '') => validator.isURL(site, { protocols: ['http', 'https'], require_protocol: true }) diff --git a/config/config.default.js b/config/config.default.js index 409896b..16a58d2 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -133,13 +133,12 @@ module.exports = appInfo => { PASS: '1' } }, - // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) + // 类型 0 文章评论 | 1 站内留言 type: { default: '0', optional: { COMMENT: '0', - MESSAGE: '1', - OTHER: '2' + MESSAGE: '1' } } } From e2fbf6de3d4aa2d73cf42f77de6e9c7e61368152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 31 Aug 2018 02:52:37 +0800 Subject: [PATCH 125/208] update: add notification controller --- app/controller/article.js | 4 -- app/controller/comment.js | 16 ++++--- app/controller/notification.js | 84 ++++++++++++++++++++++++++++++++++ app/extend/application.js | 2 +- app/model/article.js | 2 +- app/model/comment.js | 12 ++--- app/model/notification.js | 36 +++++++++++++++ app/model/user.js | 2 +- app/router/backend.js | 6 +++ app/service/auth.js | 30 ++++++------ app/service/comment.js | 37 +++++++++------ app/service/mail.js | 12 +++-- app/service/notification.js | 47 +++++++++++++++++++ app/service/proxy.js | 56 ++++++++++++++++++++++- app/service/user.js | 1 + config/config.default.js | 60 ++++++++++++++++++------ 16 files changed, 338 insertions(+), 69 deletions(-) create mode 100644 app/controller/notification.js create mode 100644 app/model/notification.js create mode 100644 app/service/notification.js diff --git a/app/controller/article.js b/app/controller/article.js index 371ef8d..b1a8d8f 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -20,10 +20,6 @@ module.exports = class ArticleController extends Controller { order: { type: 'enum', values: [-1, 1], required: false }, sortBy: { type: 'enum', values: ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], required: false } }, - item: { - // 后台用,只获取当前文章内容,不获取相关文章和上下篇文章 - single: { type: 'boolean', required: false } - }, create: { title: { type: 'string', required: true }, content: { type: 'string', required: true }, diff --git a/app/controller/comment.js b/app/controller/comment.js index 5d5365f..94900f9 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -149,7 +149,7 @@ module.exports = class CommentController extends Controller { const { ctx } = this ctx.validateCommentAuthor() const body = ctx.validateBody(this.rules.create) - const { COMMENT, MESSAGE } = type === this.config.modelValidate.comment.optional + const { COMMENT, MESSAGE } = this.config.modelValidate.comment.type.optional body.author = ctx.request.body.author const { article, parent, forward, type, content, author } = body if (type === COMMENT) { @@ -221,14 +221,16 @@ module.exports = class CommentController extends Controller { const { ctx } = this const { params } = ctx ctx.validateParamsObjectId() - ctx.validateCommentAuthor() - const body = ctx.validateBody(this.rules.create) + if (!ctx.session._isAuthed) { + ctx.validateCommentAuthor() + } + const body = ctx.validateBody(this.rules.update) body.author = ctx.request.body.author - const exist = await this.getItemById(params.id) + const exist = await this.service.comment.getItemById(params.id) if (!exist) { return ctx.fail('评论不存在') } - if (ctx.session._isAuthed && ctx.session._user._id !== exist.author._id) { + if (!ctx.session._isAuthed && ctx.session._user._id !== exist.author._id) { return ctx.fail('非本人评论不能修改') } // 状态修改是涉及到spam修改 @@ -263,7 +265,9 @@ module.exports = class CommentController extends Controller { } } } + if (body.content) { body.renderedContent = this.app.utils.markdown.render(body.content) + } let data = null if (!ctx.session._isAuthed) { data = await this.service.comment.updateItemById( @@ -284,7 +288,7 @@ module.exports = class CommentController extends Controller { ] ) } else { - data = await this.updateItemById(params.id, body) + data = await this.service.comment.updateItemById(params.id, body) } data ? ctx.success(data, '评论更新成功') diff --git a/app/controller/notification.js b/app/controller/notification.js new file mode 100644 index 0000000..aa716fe --- /dev/null +++ b/app/controller/notification.js @@ -0,0 +1,84 @@ +/** + * @desc 通告 Controller + */ + +const { Controller } = require('egg') + +module.exports = class NotificationController extends Controller { + get rules () { + return { + list: { + // 查询关键词 + page: { type: 'int', required: true, min: 1 }, + limit: { type: 'int', required: false, min: 1 }, + type: { type: 'enum', values: Object.values(this.config.modelValidate.notification.type.optional), required: false }, + classify: { type: 'enum', values: Object.values(this.config.modelValidate.notification.classify.optional), required: false }, + viewed: { type: 'boolean', required: false } + }, + view: { + viewed: { type: 'boolean', required: false } + } + } + } + + async list () { + const { ctx } = this + ctx.query.page = Number(ctx.query.page) + if (ctx.query.limit) { + ctx.query.limit = Number(ctx.query.limit) + } + ctx.validate(this.rules.list, ctx.query) + const { page, limit } = ctx.query + const options = { + sort: { + createdAt: -1 + }, + page, + limit: limit || 10, + populate: [ + { + path: 'article', + select: 'title description meta' + }, { + path: 'user', + select: 'name email role' + }, { + path: 'comment', + select: 'state spam type meta' + } + ] + } + const data = await this.service.notification.getLimitListByQuery(ctx.processPayload(ctx.query), options) + data + ? ctx.success(data, '通告列表获取成功') + : ctx.fail('通告列表获取失败') + } + + async view () { + const { ctx } = this + const params = ctx.validateParamsObjectId() + const update = { viewed: true } + const data = await this.service.notification.updateItemById(params.id, update) + data + ? ctx.success(data, '标记已读成功') + : ctx.fail('标记已读失败') + } + + async viewAll () { + const { ctx } = this + const update = { viewed: true } + const data = await this.service.notification.updateMany({}, update) + data + ? ctx.success(data, '全部标记已读成功') + : ctx.fail('全部标记已读失败') + } + + async delete () { + const { ctx } = this + const params = ctx.validateParamsObjectId() + const data = await this.service.notification.deleteItemById(params.id) + data + ? ctx.success('通告删除成功') + : ctx.fail('通告删除失败') + } +} diff --git a/app/extend/application.js b/app/extend/application.js index d659d8f..c278b50 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -10,7 +10,7 @@ module.exports = { return null } schema.set('versionKey', false) - schema.set('toObject', { getters: true }) + schema.set('toObject', { getters: true, virtuals: false }) schema.set('toJSON', { getters: true, virtuals: false }) if (options.paginate) { schema.plugin(mongoosePaginate) diff --git a/app/model/article.js b/app/model/article.js index 350cf8d..a827d09 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -26,7 +26,7 @@ module.exports = app => { thumb: { type: String, validate: app.utils.validate.isUrl }, // 文章状态 ( 0 草稿 | 1 已发布 ) state: { - type: String, + type: Number, default: articleValidateConfig.state.default, validate: val => Object.values(articleValidateConfig.state.optional).includes(val) }, diff --git a/app/model/comment.js b/app/model/comment.js index d3ca223..ed7272c 100644 --- a/app/model/comment.js +++ b/app/model/comment.js @@ -5,36 +5,32 @@ module.exports = app => { const CommentSchema = new Schema({ // ******* 评论通用项 ************ - // 创建时间 - createdAt: { type: Date, default: Date.now }, - // 修改时间 - updatedAt: { type: Date, default: Date.now }, // 评论内容 content: { type: String, required: true, validate: /\S+/ }, // marked渲染后的内容 renderedContent: { type: String, required: true, validate: /\S+/ }, // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 state: { - type: String, + type: Number, default: commentValidateConfig.state.default, validate: val => Object.values(commentValidateConfig.state.optional).includes(val) }, // Akismet判定是否是垃圾评论,方便后台check spam: { type: Boolean, default: false }, // 评论发布者 - author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + author: { type: Schema.Types.ObjectId, ref: 'User' }, // 点赞数 ups: { type: Number, default: 0, validate: /^\d*$/ }, // 是否置顶 sticky: { type: Boolean, default: false }, // 类型 0 文章评论 | 1 站内留言 | 2 其他(保留) type: { - type: String, + type: Number, default: commentValidateConfig.type.default, validate: val => Object.values(commentValidateConfig.type.optional).includes(val) }, // type为0时此项存在 - article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, + article: { type: Schema.Types.ObjectId, ref: 'Article' }, meta: { // 用户IP ip: String, diff --git a/app/model/notification.js b/app/model/notification.js new file mode 100644 index 0000000..eed3cc9 --- /dev/null +++ b/app/model/notification.js @@ -0,0 +1,36 @@ +/** + * @desc 通告模型 + */ + +module.exports = app => { + const { mongoose, config } = app + const { Schema } = mongoose + const notificationValidateConfig = config.modelValidate.notification + + const NotificationSchema = new Schema({ + // 通知类型 0 系统通知 | 1 评论通知 | 2 点赞通知 | 3 用户操作通知 + type: { + type: Number, + required: true, + validate: val => Object.values(notificationValidateConfig.type.optional).includes(val) + }, + // 类型细化分类 + classify: { + type: Number, + required: true, + validate: val => Object.values(notificationValidateConfig.classify.optional).includes(val) + }, + // 是否已读 + viewed: { type: Boolean, default: false, required: true }, + // 操作简语 + verb: { type: String, required: true, default: '' }, + // article user comment 根据情况是否包含 + article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + comment: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, + }) + + return mongoose.model('Notification', app.processSchema(NotificationSchema, { + paginate: true + })) +} diff --git a/app/model/user.js b/app/model/user.js index 19f4446..02a58ed 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -16,7 +16,7 @@ module.exports = app => { description: { type: String, default: '' }, // 角色 0 管理员 | 1 普通用户 role: { - type: String, + type: Number, default: userValidateConfig.role.default, validate: val => Object.values(userValidateConfig.role.optional).includes(val) }, diff --git a/app/router/backend.js b/app/router/backend.js index b0c6df9..a11319d 100644 --- a/app/router/backend.js +++ b/app/router/backend.js @@ -48,4 +48,10 @@ module.exports = app => { backendRouter.get('/auth/info', auth, controller.auth.info) backendRouter.patch('/auth/info', auth, controller.auth.update) backendRouter.patch('/auth/password', auth, controller.auth.password) + + // Notification + backendRouter.get('/notifications', auth, controller.notification.list) + backendRouter.patch('/notifications/view', auth, controller.notification.viewAll) + backendRouter.patch('/notifications/:id/view', auth, controller.notification.view) + backendRouter.delete('/notifications/:id', auth, controller.notification.delete) } diff --git a/app/service/auth.js b/app/service/auth.js index 6e6e533..4a30253 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -27,28 +27,30 @@ module.exports = class AuthService extends Service { */ async seed () { const ADMIN = this.config.modelValidate.user.role.optional.ADMIN - const exist = await this.service.user.getItem({ role: ADMIN }) - if (!exist) { + let admin = await this.service.user.getItem({ role: ADMIN }) + if (!admin) { const defaultAdmin = this.config.defaultAdmin - const admin = await this.service.github.getUserInfo(defaultAdmin.name) - if (admin) { - await this.service.user.create({ + const userInfo = await this.service.github.getUserInfo(defaultAdmin.name) + if (userInfo) { + admin = await this.service.user.create({ role: ADMIN, - name: admin.name, - email: admin.email || this.config.author.email, + name: userInfo.name, + email: userInfo.email || this.config.author.email, password: this.app.utils.encode.bhash(defaultAdmin.password), - slogan: admin.bio, - site: admin.blog || admin.url, - avatar: this.app.proxyUrl(admin.avatar_url), - company: admin.company, - location: admin.location, + slogan: userInfo.bio, + site: userInfo.blog || userInfo.url, + avatar: this.app.proxyUrl(userInfo.avatar_url), + company: userInfo.company, + location: userInfo.location, github: { - id: admin.id, - login: admin.login + id: userInfo.id, + login: userInfo.login } }) } } + // 挂载在session上 + this.app._admin = admin } // 更新session diff --git a/app/service/comment.js b/app/service/comment.js index ffa8cd7..a4de051 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -40,36 +40,47 @@ module.exports = class CommentService extends ProxyService { } async sendCommentEmailToAdminAndUser (comment) { + if (comment.toObject) { + comment = comment.toObject() + } const { type, article } = comment const commentType = this.config.modelValidate.comment.type.optional const permalink = this.getPermalink(comment) const adminType = this.getCommentType(comment.type) let adminTitle = '未知的评论' + let typeTitle = '' if (type === commentType.COMMENT) { // 文章评论 + typeTitle = '评论' const at = await this.service.article.getItemById(article._id || article) if (at && at._id) { adminTitle = `博客文章《${at.title}》有了新的评论` } } else if (type === commentType.MESSAGE) { // 站内留言 + typeTitle = '留言' adminTitle = '个人站点有新的留言' } - // 发送给管理员邮箱config.email - this.service.mail.sendToAdmin({ - subject: adminTitle, - text: `来自 ${comment.author.name} 的${adminType}:${comment.content}`, - html: `

来自 ${comment.author.name} 的${adminType} => 点击查看:${comment.renderedContent}

` - }) + const authorId = comment.author._id.toString() + const adminId = this.app._admin._id.toString() + const forwardAuthorId = comment.forward && comment.forward.author.toString() + // 非管理员评论,发送给管理员邮箱 + if (authorId !== adminId) { + this.service.mail.sendToAdmin(typeTitle, { + subject: adminTitle, + text: `来自 ${comment.author.name} 的${adminType}:${comment.content}`, + html: `

来自 ${comment.author.name} 的${adminType} => 点击查看:${comment.renderedContent}

` + }) + } - // 发送给被评论者 - if (comment.forward && comment.forward._id !== comment.author._id) { - const forwardAuthor = await this.service.user.findById(comment.forward.author).exec().catch(() => null) - if (forwardAuthor) { - this.service.mail.send({ - to: forwardAuthor.github.email, - subject: `你在 ${this.config.author} 的博客的评论有了新的回复`, + // 非回复管理员,非回复自身,才发送给被评论者 + if (forwardAuthorId !== authorId && forwardAuthorId !== adminId) { + const forwardAuthor = await this.service.user.getItemById(comment.forward.author).catch(() => null) + if (forwardAuthor && forwardAuthor.email) { + this.service.mail.send(typeTitle, { + to: forwardAuthor.email, + subject: `你在 ${this.config.author.name} 的博客的评论有了新的回复`, text: `来自 ${comment.author.name} 的回复:${comment.content}`, html: `

来自 ${comment.author.name} 的回复 => 点击查看:${comment.renderedContent}

` }) diff --git a/app/service/mail.js b/app/service/mail.js index 55fc0b3..3c0aeeb 100644 --- a/app/service/mail.js +++ b/app/service/mail.js @@ -8,7 +8,7 @@ let mailerClient = null module.exports = class MailService extends Service { // 发送邮件 - async send (data, toAdmin = false) { + async send (type, data, toAdmin = false) { let client = mailerClient const keys = this.app.setting.keys if (!client) { @@ -23,19 +23,21 @@ module.exports = class MailService extends Service { if (toAdmin) { opt.to = keys.mail.user } + type = type ? `[${type}]` : '' + toAdmin = toAdmin ? '管理员' : '' await new Promise((resolve, reject) => { client.sendMail(opt, (err, info) => { if (err) { - this.logger.error('邮件发送失败,' + err.message) + this.logger.error(type + toAdmin + '邮件发送失败,TO:' + opt.to + ',错误:' + err.message) return reject(err) } - this.logger.info('邮件发送成功,TO:' + opt.to) + this.logger.info(type + toAdmin + '邮件发送成功,TO:' + opt.to) resolve(info) }) }) } - sendToAdmin (data) { - return this.send(data, true) + sendToAdmin (type, data) { + return this.send(type, data, true) } } diff --git a/app/service/notification.js b/app/service/notification.js new file mode 100644 index 0000000..b787393 --- /dev/null +++ b/app/service/notification.js @@ -0,0 +1,47 @@ +/** + * @desc 通告 Services + */ + +const ProxyService = require('./proxy') + +module.exports = class NotificationService extends ProxyService { + get model () { + return this.app.model.Notification + } + + // 记录通告 + async record (type, classify, target = {}) { + const notificationConfig = this.config.modelValidate.notification + type = notificationConfig.type.optional[type] + classify = notificationConfig.classify.optional[classify] + const payload = Object.assign({ type, classify }, target) + const data = await this.create(payload) + if (data) { + this.logger.info(`通过生成成功,type:${type},classify: ${type}`) + } + } + + getTypeByModel (model, action) { + const classifyMap = Object.keys(this.config.modelValidate.notification.classify.optional) + const typeMap = new Map() + const modelSet = classifyMap.reduce((set, key) => { + const frag = key.split('_') + set.add(frag[0]) + const type = frag.pop() + typeMap.set(frag.join('_'), Number(type)) + return set + }, new Set()) + const modelName = model.modelName.toLocaleUpperCase() + if (!modelSet.has(modelName)) return null + const classifyPrefix = modelName + '_' + action + const type = typeMap.get(classifyPrefix) + if (!type) return null + return { + type, + classify: classifyPrefix + '_' + type + } + } + + // 获取操作简语 + getVerb () {} +} diff --git a/app/service/proxy.js b/app/service/proxy.js index c9b84f1..4830529 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -47,8 +47,52 @@ module.exports = class ProxyService extends Service { return Q.exec() } - create (data) { - return this.model.create(data) + async create (payload) { + const data = await this.model.create(payload) + const modelName = this.model.modelName + // FIX: 触发通告,待优化 + const n = this.service.notification + const record = n.record.bind(n) + const target = {} + let action = '' + if (modelName === 'Comment') { + target.comment = data._id + // 评论|留言创建 + if (data._id !== this.app._admin._id) { + // 非管理员才触发 + const { COMMENT, MESSAGE } = this.config.modelValidate.comment.type.optional + if (data.type === COMMENT) { + // 文章评论 + if (data.parent) { + // 评论回复 + action = 'COMMENT_REPLY' + } else { + // 评论,非回复 + action = 'COMMENT' + } + } else if (data.type === MESSAGE) { + // 站内留言 + if (data.parent) { + // 留言回复 + action = 'MESSAGE_REPLY' + } else { + // 留言,非回复 + action = 'MESSAGE' + } + } + } + } else if (modelName === 'User') { + target.user = data._id + // 用户创建 + const { ADMIN } = this.config.modelValidate.user.role.optional + if (data.role !== ADMIN) { + // 非管理员才触发 + action = 'CREATE' + } + } + const type = this.service.notification.getTypeByModel(this.model, action) + record(type.type, type.classify, target) + return data } updateItem (query = {}, data, opt, populate = []) { @@ -75,6 +119,14 @@ module.exports = class ProxyService extends Service { return Q.exec() } + updateMany (query, data, opt) { + return this.model.updateMany(query, data, opt) + } + + updateManyById (id, data, opt) { + return this.updateMany({ _id: id }, data, opt) + } + deleteItemById (id, opt) { return this.model.findByIdAndDelete(id, opt).exec() } diff --git a/app/service/user.js b/app/service/user.js index e4c2d36..09c1fb9 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -14,6 +14,7 @@ module.exports = class UserService extends ProxyService { const { name } = user const exist = await this.getItem({ name }) if (exist) { + this.logger.info('用户已存在,无需创建:' + name) return exist } const data = await new this.model(user).save() diff --git a/config/config.default.js b/config/config.default.js index 16a58d2..1f2613a 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -105,40 +105,72 @@ module.exports = appInfo => { article: { // 文章状态 ( 0 草稿(默认) | 1 已发布 ) state: { - default: '0', + default: 0, optional: { - DRAFT: '0', - PUBLISH: '1' + DRAFT: 0, + PUBLISH: 1 } } }, user: { // 角色 0 管理员 | 1 普通用户 role: { - default: '1', + default: 1, optional: { - ADMIN: '0', - NORMAL: '1' + ADMIN: 0, + NORMAL: 1 } } }, comment: { // 状态 -2 垃圾评论 | -1 已删除 | 0 待审核 | 1 通过 state: { - default: '1', + default: 1, optional: { - SPAM: '-2', - DELETED: '-1', - AUDITING: '0', - PASS: '1' + SPAM: -2, + DELETED: -1, + AUDITING: 0, + PASS: 1 } }, // 类型 0 文章评论 | 1 站内留言 type: { - default: '0', + default: 0, optional: { - COMMENT: '0', - MESSAGE: '1' + COMMENT: 0, + MESSAGE: 1 + } + } + }, + notification: { + type: { + optional: { + GENERAL: 0, + COMMENT: 1, + LIKE: 2, + USER: 3 + } + }, + classify: { + optional: { + // type === 0,系统通知 + // todo + // type === 1,评论通知 + COMMENT_COMMENT_1: 'comment_comment', // 评论(非回复) + COMMENT_COMMENT_REPLY_1: 'comment_comment_reply', // 评论回复 + COMMENT_COMMENT_UPDATE_1: 'comment_update', // 评论更新(保留) + COMMENT_MESSAGE_1: 'comment_message', // 站内留言 + COMMENT_MESSAGE_REPLY_1: 'comment_message_reply', // 站内留言回复 + COMMENT_MESSAGE_UPDATE_1: 'comment_message_reply', // 站内留言更新 + // type === 2,点赞通知 + ARTICLE_LIKE_2: 'article_like', // 文章点赞 + ARTICLE_UNLIKE_2: 'article_unlike', // 文章取消点赞 + COMMENT_LIKE_2: 'coment_like', // 评论点赞 + COMMENT_UNLIKE_2: 'comment_unlike', // 评论取消点赞 + // type === 3, 用户操作通知 + USER_MUTE_AUTO_3: 'user_mute_auto', // 用户被自动禁言 + USER_CREATE_3: 'user_create', // 用户创建 + USER_UPDATE_3: 'user_update' // 用户更新 } } } From 7615ac7323b36ba22b47a7c40d71b95bc7facfc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 31 Aug 2018 18:28:41 +0800 Subject: [PATCH 126/208] update: notification service done --- app/controller/article.js | 46 ++++++++--- app/controller/comment.js | 110 ++++++++++++++++--------- app/controller/notification.js | 44 ++++++---- app/controller/setting.js | 2 - app/middleware/auth.js | 2 +- app/model/article.js | 8 +- app/model/comment.js | 2 +- app/model/notification.js | 18 +++-- app/model/user.js | 2 +- app/router/backend.js | 4 +- app/router/frontend.js | 3 + app/service/article.js | 8 +- app/service/auth.js | 2 +- app/service/category.js | 2 +- app/service/comment.js | 8 +- app/service/notification.js | 143 +++++++++++++++++++++++++++------ app/service/proxy.js | 46 +---------- app/service/tag.js | 2 +- app/service/user.js | 25 ++++-- config/config.default.js | 39 +++++---- 20 files changed, 340 insertions(+), 176 deletions(-) diff --git a/app/controller/article.js b/app/controller/article.js index b1a8d8f..47406fa 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -10,7 +10,8 @@ module.exports = class ArticleController extends Controller { list: { page: { type: 'int', required: true, min: 1 }, limit: { type: 'int', required: false, min: 1 }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: false }, + source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: false }, category: { type: 'objectId', required: false }, tag: { type: 'objectId', required: false }, keyword: { type: 'string', required: false }, @@ -27,7 +28,8 @@ module.exports = class ArticleController extends Controller { keywords: { type: 'array', required: false }, category: { type: 'objectId', required: false }, tag: { type: 'array', required: false, itemType: 'objectId' }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: false }, + source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: false }, thumb: { type: 'url', required: false }, createdAt: { type: 'dateTime', required: false } }, @@ -38,7 +40,8 @@ module.exports = class ArticleController extends Controller { keywords: { type: 'array', required: false }, category: { type: 'objectId', required: false }, tag: { type: 'array', required: false, itemType: 'objectId' }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.article.state.optional), required: false }, + state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: false }, + source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: false }, thumb: { type: 'url', required: false }, createdAt: { type: 'dateTime', required: false } } @@ -48,9 +51,12 @@ module.exports = class ArticleController extends Controller { async list () { const { ctx } = this ctx.query.page = Number(ctx.query.page) - if (ctx.query.limit) { - ctx.query.limit = Number(ctx.query.limit) - } + const tranArray = ['limit', 'state', 'source'] + tranArray.forEach(key => { + if (ctx.query[key]) { + ctx.query[key] = Number(ctx.query[key]) + } + }) ctx.validate(this.rules.list, ctx.query) const { page, limit, state, keyword, category, tag, order, sortBy, startDate, endDate } = ctx.query const options = { @@ -185,11 +191,33 @@ module.exports = class ArticleController extends Controller { 'meta.ups': 1 } }) - data - ? ctx.success('文章点赞成功') - : ctx.fail('文章点赞失败') + if (data) { + // 生成like通告 + this.service.notification.recordLike('article', data, ctx.request.body.user, true) + ctx.success('文章点赞成功') + } else { + ctx.fail('文章点赞失败') + } } + async unlike () { + const { ctx } = this + const params = ctx.validateParamsObjectId() + const data = await this.service.article.updateItemById(params.id, { + $inc: { + 'meta.ups': -1 + } + }) + if (data) { + // 生成unlike通告 + this.service.notification.recordLike('article', data, ctx.request.body.user, false) + ctx.success('文章取消点赞成功') + } else { + ctx.fail('文章取消点赞失败') + } + } + + async archives () { this.ctx.success(await this.service.article.archives(), '归档获取成功') } diff --git a/app/controller/comment.js b/app/controller/comment.js index 94900f9..53fbffe 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -10,8 +10,8 @@ module.exports = class CommentController extends Controller { list: { page: { type: 'int', required: true, min: 1 }, limit: { type: 'int', required: false, min: 1 }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, - type: { type: 'enum', values: Object.values(this.config.modelValidate.comment.type.optional), required: false }, + state: { type: 'enum', values: Object.values(this.config.modelEnum.comment.state.optional), required: false }, + type: { type: 'enum', values: Object.values(this.config.modelEnum.comment.type.optional), required: false }, author: { type: 'objectId', required: false }, article: { type: 'objectId', required: false }, parent: { type: 'objectId', required: false }, @@ -26,14 +26,14 @@ module.exports = class CommentController extends Controller { }, create: { content: { type: 'string', required: true }, - type: { type: 'enum', values: Object.values(this.config.modelValidate.comment.type.optional), required: true }, + type: { type: 'enum', values: Object.values(this.config.modelEnum.comment.type.optional), required: true }, article: { type: 'objectId', required: false }, parent: { type: 'objectId', required: false }, forward: { type: 'objectId', required: false } }, update: { content: { type: 'string', required: false }, - state: { type: 'enum', values: Object.values(this.config.modelValidate.comment.state.optional), required: false }, + state: { type: 'enum', values: Object.values(this.config.modelEnum.comment.state.optional), required: false }, sticky: { type: 'boolean', required: false } } } @@ -42,9 +42,12 @@ module.exports = class CommentController extends Controller { async list () { const { ctx } = this ctx.query.page = Number(ctx.query.page) - if (ctx.query.limit) { - ctx.query.limit = Number(ctx.query.limit) - } + const tranArray = ['limit', 'state', 'type', 'order', 'sortBy'] + tranArray.forEach(key => { + if (ctx.query[key]) { + ctx.query[key] = Number(ctx.query[key]) + } + }) ctx.validate(this.rules.list, ctx.query) const { page, limit, state, type, keyword, author, article, parent, order, sortBy, startDate, endDate } = ctx.query // 过滤条件 @@ -149,7 +152,7 @@ module.exports = class CommentController extends Controller { const { ctx } = this ctx.validateCommentAuthor() const body = ctx.validateBody(this.rules.create) - const { COMMENT, MESSAGE } = this.config.modelValidate.comment.type.optional + const { COMMENT, MESSAGE } = this.config.modelEnum.comment.type.optional body.author = ctx.request.body.author const { article, parent, forward, type, content, author } = body if (type === COMMENT) { @@ -163,9 +166,9 @@ module.exports = class CommentController extends Controller { if ((parent && !forward) || (!parent && forward)) { return ctx.fail(422, '缺少父评论ID或被回复评论ID') } - const user = await this.service.user.checkCommentAuthor(author) + const { user, error } = await this.service.user.checkCommentAuthor(author) if (!user) { - return ctx.fail('用户不存在') + return ctx.fail(error) } else if (user.mute) { // 被禁言 return ctx.fail('该用户已被禁言') @@ -194,11 +197,12 @@ module.exports = class CommentController extends Controller { comment_author_email: user.email, comment_author_url: user.site, comment_content: content, - is_test: this.config.isProd + is_test: !this.config.isProd }) // 如果是Spam评论 if (isSpam) { - return ctx.fail('检测为垃圾评论,该评论将不会显示') + this.logger.warn('检测为垃圾评论,禁止发布') + return ctx.fail('检测为垃圾评论,请修改后在提交') } this.logger.info('评论检测正常,可以发布') body.renderedContent = this.app.utils.markdown.render(body.content) @@ -207,10 +211,12 @@ module.exports = class CommentController extends Controller { const data = await this.service.comment.getItemById(comment._id) if (data.type === COMMENT) { // 如果是文章评论,则更新文章评论数量 - this.service.article.updateCommentCount(data.article) + this.service.article.updateCommentCount(data.article._id) } // 发送邮件通知站主和被评论者 this.service.comment.sendCommentEmailToAdminAndUser(data) + // 生成通告 + this.service.notification.recordComment(comment, 'create') ctx.success(data, data.type === COMMENT ? '评论发布成功' : '留言发布成功') } else { ctx.fail('发布失败') @@ -233,22 +239,29 @@ module.exports = class CommentController extends Controller { if (!ctx.session._isAuthed && ctx.session._user._id !== exist.author._id) { return ctx.fail('非本人评论不能修改') } + const permalink = this.service.comment.getPermalink(exist) + const opt = { + user_ip: exist.meta.ip, + user_agent: exist.meta.ua, + referrer: exist.meta.referer, + permalink, + comment_type: this.service.comment.getCommentType(exist.type), + comment_author: exist.author.github.login, + comment_author_email: exist.author.github.email, + comment_author_url: exist.author.github.blog, + comment_content: exist.content, + is_test: !this.config.isProd + } + const isSpam = await this.app.akismet.checkSpam(opt) + // 如果是Spam评论 + if (isSpam) { + this.logger.warn('检测为垃圾评论,禁止发布') + return ctx.fail('检测为垃圾评论,不能更新') + } + this.logger.info('评论检测正常,可以更新') // 状态修改是涉及到spam修改 if (body.state !== undefined) { - const permalink = this.service.comment.getPermalink(exist) - const opt = { - user_ip: exist.meta.ip, - user_agent: exist.meta.ua, - referrer: exist.meta.referer, - permalink, - comment_type: this.service.comment.getCommentType(exist.type), - comment_author: exist.author.github.login, - comment_author_email: exist.author.github.email, - comment_author_url: exist.author.github.blog, - comment_content: exist.content, - is_test: this.config.isProd - } - const SPAM = this.config.modelValidate.comment.state.optional.SPAM + const SPAM = this.config.modelEnum.comment.state.optional.SPAM if (exist.state === SPAM && body.state !== SPAM) { // 垃圾评论转为正常评论 if (exist.spam) { @@ -266,7 +279,7 @@ module.exports = class CommentController extends Controller { } } if (body.content) { - body.renderedContent = this.app.utils.markdown.render(body.content) + body.renderedContent = this.app.utils.markdown.render(body.content) } let data = null if (!ctx.session._isAuthed) { @@ -290,18 +303,22 @@ module.exports = class CommentController extends Controller { } else { data = await this.service.comment.updateItemById(params.id, body) } - data - ? ctx.success(data, '评论更新成功') - : ctx.fail('评论更新失败') + if (data) { + // 生成通告 + this.service.notification.recordComment(data, 'update') + ctx.success(data, '评论更新成功') + } else { + ctx.fail('评论更新失败') + } } async delete () { const { ctx } = this const params = ctx.validateParamsObjectId() const data = await this.service.comment.deleteItemById(params.id) - if (data.type === this.config.modelValidate.comment.type.optional.COMMENT) { + if (data.type === this.config.modelEnum.comment.type.optional.COMMENT) { // 异步 如果是文章评论,则更新文章评论数量 - this.service.article.updateCommentCount(data.article) + this.service.article.updateCommentCount(data.article._id) } data ? ctx.success('评论删除成功') @@ -316,8 +333,29 @@ module.exports = class CommentController extends Controller { ups: 1 } }) - data - ? ctx.success('评论点赞成功') - : ctx.fail('评论点赞失败') + if (data) { + // 生成评论点赞通告 + this.service.notification.recordLike('comment', data, ctx.request.body.user, true) + ctx.success('评论点赞成功') + } else { + ctx.fail('评论点赞失败') + } + } + + async unlike () { + const { ctx } = this + const params = ctx.validateParamsObjectId() + const data = await this.service.comment.updateItemById(params.id, { + $inc: { + ups: -1 + } + }) + if (data) { + // 生成评论unlike通告 + this.service.notification.recordLike('comment', data, ctx.request.body.user, false) + ctx.success('评论取消点赞成功') + } else { + ctx.fail('评论取消点赞失败') + } } } diff --git a/app/controller/notification.js b/app/controller/notification.js index aa716fe..6442afa 100644 --- a/app/controller/notification.js +++ b/app/controller/notification.js @@ -11,11 +11,8 @@ module.exports = class NotificationController extends Controller { // 查询关键词 page: { type: 'int', required: true, min: 1 }, limit: { type: 'int', required: false, min: 1 }, - type: { type: 'enum', values: Object.values(this.config.modelValidate.notification.type.optional), required: false }, - classify: { type: 'enum', values: Object.values(this.config.modelValidate.notification.classify.optional), required: false }, - viewed: { type: 'boolean', required: false } - }, - view: { + type: { type: 'enum', values: Object.values(this.config.modelEnum.notification.type.optional), required: false }, + classify: { type: 'enum', values: Object.values(this.config.modelEnum.notification.classify.optional), required: false }, viewed: { type: 'boolean', required: false } } } @@ -24,11 +21,18 @@ module.exports = class NotificationController extends Controller { async list () { const { ctx } = this ctx.query.page = Number(ctx.query.page) - if (ctx.query.limit) { - ctx.query.limit = Number(ctx.query.limit) + const tranArray = ['limit', 'type'] + tranArray.forEach(key => { + if (ctx.query[key]) { + ctx.query[key] = Number(ctx.query[key]) + } + }) + if (ctx.query.viewed) { + ctx.query.viewed = ctx.query.viewed === 'true' } ctx.validate(this.rules.list, ctx.query) - const { page, limit } = ctx.query + const { page, limit, type, classify, viewed } = ctx.query + const query = { type, classify, viewed } const options = { sort: { createdAt: -1 @@ -37,18 +41,24 @@ module.exports = class NotificationController extends Controller { limit: limit || 10, populate: [ { - path: 'article', + path: 'target.article', select: 'title description meta' }, { - path: 'user', - select: 'name email role' + path: 'target.user', + select: 'name email role github' }, { - path: 'comment', + path: 'target.comment', select: 'state spam type meta' + }, { + path: 'actors.from', + select: 'name email github' + }, { + path: 'actors.to', + select: 'name email github' } ] } - const data = await this.service.notification.getLimitListByQuery(ctx.processPayload(ctx.query), options) + const data = await this.service.notification.getLimitListByQuery(ctx.processPayload(query), options) data ? ctx.success(data, '通告列表获取成功') : ctx.fail('通告列表获取失败') @@ -60,8 +70,8 @@ module.exports = class NotificationController extends Controller { const update = { viewed: true } const data = await this.service.notification.updateItemById(params.id, update) data - ? ctx.success(data, '标记已读成功') - : ctx.fail('标记已读失败') + ? ctx.success('通告标记已读成功') + : ctx.fail('通告标记已读失败') } async viewAll () { @@ -69,8 +79,8 @@ module.exports = class NotificationController extends Controller { const update = { viewed: true } const data = await this.service.notification.updateMany({}, update) data - ? ctx.success(data, '全部标记已读成功') - : ctx.fail('全部标记已读失败') + ? ctx.success('通告全部标记已读成功') + : ctx.fail('通告全部标记已读失败') } async delete () { diff --git a/app/controller/setting.js b/app/controller/setting.js index 57b0a62..4a91885 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -40,8 +40,6 @@ module.exports = class SettingController extends Controller { return ctx.fail('配置未找到') } body = this.app.merge({}, exist, body) - console.log(body); - await this.service.setting.updateItemById(exist._id, body) // 抓取友链 const data = await this.service.setting.updateLinks() diff --git a/app/middleware/auth.js b/app/middleware/auth.js index 4b1e81a..11a2fee 100644 --- a/app/middleware/auth.js +++ b/app/middleware/auth.js @@ -20,7 +20,7 @@ module.exports = app => { return ctx.fail(401, '用户不存在') } ctx.session._user = user - ctx.session._isAdmin = user.role === app.config.modelValidate.user.role.optional.ADMIN + ctx.session._isAdmin = user.role === app.config.modelEnum.user.role.optional.ADMIN ctx.session._isAuthed = true await next() } diff --git a/app/model/article.js b/app/model/article.js index a827d09..0652a3b 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -5,7 +5,7 @@ module.exports = app => { const { mongoose, config } = app const { Schema } = mongoose - const articleValidateConfig = config.modelValidate.article + const articleValidateConfig = config.modelEnum.article const ArticleSchema = new Schema({ // 文章标题 @@ -24,6 +24,12 @@ module.exports = app => { tag: [{ type: Schema.Types.ObjectId, ref: 'Tag' }], // 缩略图 (图片uid, 图片名称,图片URL, 图片大小) thumb: { type: String, validate: app.utils.validate.isUrl }, + // 来源 0 原创 | 1 转载 + source: { + type: Number, + default: articleValidateConfig.source.default, + validate: val => Object.values(articleValidateConfig.source.optional).includes(val) + }, // 文章状态 ( 0 草稿 | 1 已发布 ) state: { type: Number, diff --git a/app/model/comment.js b/app/model/comment.js index ed7272c..233322f 100644 --- a/app/model/comment.js +++ b/app/model/comment.js @@ -1,7 +1,7 @@ module.exports = app => { const { mongoose, config } = app const { Schema } = mongoose - const commentValidateConfig = config.modelValidate.comment + const commentValidateConfig = config.modelEnum.comment const CommentSchema = new Schema({ // ******* 评论通用项 ************ diff --git a/app/model/notification.js b/app/model/notification.js index eed3cc9..42ffaf8 100644 --- a/app/model/notification.js +++ b/app/model/notification.js @@ -5,7 +5,7 @@ module.exports = app => { const { mongoose, config } = app const { Schema } = mongoose - const notificationValidateConfig = config.modelValidate.notification + const notificationValidateConfig = config.modelEnum.notification const NotificationSchema = new Schema({ // 通知类型 0 系统通知 | 1 评论通知 | 2 点赞通知 | 3 用户操作通知 @@ -16,7 +16,7 @@ module.exports = app => { }, // 类型细化分类 classify: { - type: Number, + type: String, required: true, validate: val => Object.values(notificationValidateConfig.classify.optional).includes(val) }, @@ -24,10 +24,16 @@ module.exports = app => { viewed: { type: Boolean, default: false, required: true }, // 操作简语 verb: { type: String, required: true, default: '' }, - // article user comment 根据情况是否包含 - article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, - user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, - comment: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, + target: { + // article user comment 根据情况是否包含 + article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + comment: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, + }, + actors: { + from: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + to: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } + } }) return mongoose.model('Notification', app.processSchema(NotificationSchema, { diff --git a/app/model/user.js b/app/model/user.js index 02a58ed..5e31cd9 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -5,7 +5,7 @@ module.exports = app => { const { mongoose, config } = app const { Schema } = mongoose - const userValidateConfig = config.modelValidate.user + const userValidateConfig = config.modelEnum.user const UserSchema = new Schema({ name: { type: String, required: true }, diff --git a/app/router/backend.js b/app/router/backend.js index a11319d..8294c84 100644 --- a/app/router/backend.js +++ b/app/router/backend.js @@ -10,6 +10,7 @@ module.exports = app => { backendRouter.post('/articles', auth, controller.article.create) backendRouter.patch('/articles/:id', auth, controller.article.update) backendRouter.patch('/articles/:id/like', auth, controller.article.like) + backendRouter.patch('/articles/:id/unlike', auth, controller.article.unlike) backendRouter.delete('/articles/:id', auth, controller.article.delete) // Category @@ -31,8 +32,9 @@ module.exports = app => { backendRouter.get('/comments/:id', auth, controller.comment.item) backendRouter.post('/comments', auth, controller.comment.create) backendRouter.patch('/comments/:id', auth, controller.comment.update) - backendRouter.delete('/comments/:id', auth, controller.comment.delete) backendRouter.patch('/comments/:id/like', auth, controller.comment.like) + backendRouter.patch('/comments/:id/unlike', auth, controller.comment.unlike) + backendRouter.delete('/comments/:id', auth, controller.comment.delete) // User backendRouter.get('/users', auth, controller.user.list) diff --git a/app/router/frontend.js b/app/router/frontend.js index cff8d19..66c6e7b 100644 --- a/app/router/frontend.js +++ b/app/router/frontend.js @@ -7,6 +7,8 @@ module.exports = app => { fontendRouter.get('/articles/archives', controller.article.archives) fontendRouter.get('/articles/:id', controller.article.item) fontendRouter.patch('/articles/:id', controller.article.like) + fontendRouter.patch('/articles/:id/like', controller.article.like) + fontendRouter.patch('/articles/:id/unlike', controller.article.unlike) // Category fontendRouter.get('/categories', controller.category.list) @@ -21,6 +23,7 @@ module.exports = app => { fontendRouter.get('/comments/:id', controller.comment.item) fontendRouter.post('/comments', controller.comment.create) fontendRouter.patch('/comments/:id/like', controller.comment.like) + fontendRouter.patch('/comments/:id/unlike', controller.comment.unlike) // User fontendRouter.get('/users/:id', controller.user.item) diff --git a/app/service/article.js b/app/service/article.js index 8e4f390..b526dce 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -15,7 +15,7 @@ module.exports = class ArticleService extends ProxyService { if (!this.ctx.session._isAuthed) { api = this.updateItem.bind(this) // 前台博客访问文章的时候pv+1 - query.state = this.config.modelValidate.article.state.optional.PUBLISH + query.state = this.config.modelEnum.article.state.optional.PUBLISH select += ' -content' opt.$inc = { 'meta.pvs': 1 } } @@ -126,7 +126,7 @@ module.exports = class ArticleService extends ProxyService { } // 如果未通过权限校验,将文章状态重置为1 if (!this.ctx.session._isAuthed) { - query.state = this.config.modelValidate.article.state.optional.PUBLISH + query.state = this.config.modelEnum.article.state.optional.PUBLISH } const prev = await this.getItem( query, @@ -165,7 +165,6 @@ module.exports = class ArticleService extends ProxyService { } } - async updateCommentCount (articleIds = []) { if (!Array.isArray(articleIds)) { articleIds = [articleIds] @@ -174,9 +173,8 @@ module.exports = class ArticleService extends ProxyService { const { validate, share } = this.app.utils // TIP: 这里必须$in的是一个ObjectId对象数组,而不能只是id字符串数组 articleIds = [...new Set(articleIds)].filter(id => validate.isObjectId(id)).map(id => share.createObjectId(id)) - const PASS = this.config.modelValidate.comment.state.optional.PASS const counts = await this.service.comment.aggregate([ - { $match: { state: PASS, article: { $in: articleIds } } }, + { $match: { article: { $in: articleIds } } }, { $group: { _id: '$article', total_count: { $sum: 1 } } } ]) Promise.all( diff --git a/app/service/auth.js b/app/service/auth.js index 4a30253..c13acc9 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -26,7 +26,7 @@ module.exports = class AuthService extends Service { * @desc 创建管理员,用于server初始化时 */ async seed () { - const ADMIN = this.config.modelValidate.user.role.optional.ADMIN + const ADMIN = this.config.modelEnum.user.role.optional.ADMIN let admin = await this.service.user.getItem({ role: ADMIN }) if (!admin) { const defaultAdmin = this.config.defaultAdmin diff --git a/app/service/category.js b/app/service/category.js index 7c74d9c..1c49a12 100644 --- a/app/service/category.js +++ b/app/service/category.js @@ -15,7 +15,7 @@ module.exports = class CategoryService extends ProxyService { }, opt) const categories = await this.model.find(query, select, opt).exec() if (categories.length) { - const PUBLISH = this.app.config.modelValidate.article.state.optional.PUBLISH + const PUBLISH = this.app.config.modelEnum.article.state.optional.PUBLISH await Promise.all( categories.map(async item => { const articles = await this.service.article.getList({ diff --git a/app/service/comment.js b/app/service/comment.js index a4de051..d21dd77 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -44,7 +44,7 @@ module.exports = class CommentService extends ProxyService { comment = comment.toObject() } const { type, article } = comment - const commentType = this.config.modelValidate.comment.type.optional + const commentType = this.config.modelEnum.comment.type.optional const permalink = this.getPermalink(comment) const adminType = this.getCommentType(comment.type) let adminTitle = '未知的评论' @@ -75,8 +75,8 @@ module.exports = class CommentService extends ProxyService { } // 非回复管理员,非回复自身,才发送给被评论者 - if (forwardAuthorId !== authorId && forwardAuthorId !== adminId) { - const forwardAuthor = await this.service.user.getItemById(comment.forward.author).catch(() => null) + if (forwardAuthorId && forwardAuthorId !== authorId && forwardAuthorId !== adminId) { + const forwardAuthor = await this.service.user.getItemById(forwardAuthorId).catch(() => null) if (forwardAuthor && forwardAuthor.email) { this.service.mail.send(typeTitle, { to: forwardAuthor.email, @@ -95,7 +95,7 @@ module.exports = class CommentService extends ProxyService { */ getPermalink (comment = {}) { const { type, article } = comment - const { COMMENT, MESSAGE } = this.config.modelValidate.comment.type.optional + const { COMMENT, MESSAGE } = this.config.modelEnum.comment.type.optional const url = this.config.author.url switch (type) { case COMMENT: diff --git a/app/service/notification.js b/app/service/notification.js index b787393..8b200d5 100644 --- a/app/service/notification.js +++ b/app/service/notification.js @@ -9,39 +9,132 @@ module.exports = class NotificationService extends ProxyService { return this.app.model.Notification } + get notificationConfig () { + return this.config.modelEnum.notification + } + // 记录通告 - async record (type, classify, target = {}) { - const notificationConfig = this.config.modelValidate.notification - type = notificationConfig.type.optional[type] - classify = notificationConfig.classify.optional[classify] - const payload = Object.assign({ type, classify }, target) + async record (typeKey, model, action, target, actors) { + if (!typeKey || !model || !action) return + const modelName = this.app.utils.validate.isString(model) + ? model + : model.modelName.toLocaleUpperCase() + const type = this.notificationConfig.type.optional[typeKey] + const classifyKey = [typeKey, modelName, action].join('_') + const classify = this.notificationConfig.classify.optional[classifyKey] + const verb = this.genVerb(classifyKey) + const payload = { type, classify, verb, target, actors } const data = await this.create(payload) if (data) { - this.logger.info(`通过生成成功,type:${type},classify: ${type}`) + this.logger.info(`通告生成成功,[id: ${data._id}] [type:${typeKey}],[classify: ${classifyKey}]`) + } + } + + // 记录评论相关动作 + async recordComment (comment, handle = 'create') { + if (!comment || !comment._id) return + const target = {} + const actors = {} + let action = '' + comment = await this.service.comment.getItemById(comment._id) + if (!comment) return + const { COMMENT, MESSAGE } = this.config.modelEnum.comment.type.optional + const { type, forward, article, author } = comment + actors.from = author._id || author + target.comment = comment._id + if (type === COMMENT) { + // 文章评论 + action += 'COMMENT' + if (handle === 'create') { + target.article = article._id || article + } + } else if (type === MESSAGE) { + // 站内留言 + action += 'MESSAGE' + } + if (handle === 'create') { + if (forward) { + action += '_REPLY' + const forwardId = forward._id || forward + target.comment = forwardId + const forwardItem = await this.service.comment.getItemById(forwardId) + actors.to = forwardItem.author._id + } + } else if (handle === 'update') { + // 更新 + action += '_UPDATE' + } + this.record('COMMENT', 'COMMENT', action, target, actors) + } + + recordLike (type, model, user, like = false) { + const { COMMENT, MESSAGE } = this.config.modelEnum.comment.type.optional + let modelName = '' + let action = '' + const actionSuffix = like ? 'LIKE' : 'UNLIKE' + const target = {} + const actors = {} + if (user) { + actors.from = user._id || user } + if (type === 'article') { + // 文章 + modelName = 'ARTICLE' + target.article = model._id + } else if (type === 'comment') { + // 评论 + modelName = 'COMMENT' + target.comment = model._id + if (model.type === COMMENT) { + action += 'COMMENT_' + } else if (model.type === MESSAGE) { + action += 'MESSAGE_' + } + } + action += actionSuffix + this.record('LIKE', modelName, action, target, actors) } - getTypeByModel (model, action) { - const classifyMap = Object.keys(this.config.modelValidate.notification.classify.optional) - const typeMap = new Map() - const modelSet = classifyMap.reduce((set, key) => { - const frag = key.split('_') - set.add(frag[0]) - const type = frag.pop() - typeMap.set(frag.join('_'), Number(type)) - return set - }, new Set()) - const modelName = model.modelName.toLocaleUpperCase() - if (!modelSet.has(modelName)) return null - const classifyPrefix = modelName + '_' + action - const type = typeMap.get(classifyPrefix) - if (!type) return null - return { - type, - classify: classifyPrefix + '_' + type + recordUser (user, handle) { + let action = '' + const target = { + user: user._id || user + } + const actors = { + from: target.user } + if (handle === 'create') { + action += 'CREATE' + } else if (handle === 'update') { + action += 'UPDATE' + } else if (handle === 'mute') { + action += 'MUTE_AUTO' + } + this.record('USER', 'USER', action, target, actors) } // 获取操作简语 - getVerb () {} + genVerb (classify) { + const verbMap = { + // type === 1,评论通知 + COMMENT_COMMENT_COMMENT: '评论了文章', + COMMENT_COMMENT_COMMENT_REPLY: '回复了评论', + COMMENT_COMMENT_COMMENT_UPDATE: '更新了评论', + COMMENT_COMMENT_MESSAGE: '在站内留言', + COMMENT_COMMENT_MESSAGE_REPLY: '回复了留言', + COMMENT_COMMENT_MESSAGE_UPDATE: '更新了留言', + // type === 2,点赞通知 + LIKE_ARTICLE_LIKE: '给文章点了赞', + LIKE_ARTICLE_UNLIKE: '取消了文章点赞', + LIKE_COMMENT_COMMENT_LIKE: '给评论点了赞', + LIKE_COMMENT_MESSAGE_LIKE: '给留言点了赞', + LIKE_COMMENT_COMMENT_UNLIKE: '取消了评论点赞', + LIKE_COMMENT_MESSAGE_UNLIKE: '取消了留言点赞', + // type === 3, 用户操作通知 + USER_USER_MUTE_AUTO: '用户被自动禁言', + USER_USER_CREATE: '新增用户', + USER_USER_UPDATE: '更新用户信息' + } + return verbMap[classify] + } } diff --git a/app/service/proxy.js b/app/service/proxy.js index 4830529..50fdf60 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -48,51 +48,7 @@ module.exports = class ProxyService extends Service { } async create (payload) { - const data = await this.model.create(payload) - const modelName = this.model.modelName - // FIX: 触发通告,待优化 - const n = this.service.notification - const record = n.record.bind(n) - const target = {} - let action = '' - if (modelName === 'Comment') { - target.comment = data._id - // 评论|留言创建 - if (data._id !== this.app._admin._id) { - // 非管理员才触发 - const { COMMENT, MESSAGE } = this.config.modelValidate.comment.type.optional - if (data.type === COMMENT) { - // 文章评论 - if (data.parent) { - // 评论回复 - action = 'COMMENT_REPLY' - } else { - // 评论,非回复 - action = 'COMMENT' - } - } else if (data.type === MESSAGE) { - // 站内留言 - if (data.parent) { - // 留言回复 - action = 'MESSAGE_REPLY' - } else { - // 留言,非回复 - action = 'MESSAGE' - } - } - } - } else if (modelName === 'User') { - target.user = data._id - // 用户创建 - const { ADMIN } = this.config.modelValidate.user.role.optional - if (data.role !== ADMIN) { - // 非管理员才触发 - action = 'CREATE' - } - } - const type = this.service.notification.getTypeByModel(this.model, action) - record(type.type, type.classify, target) - return data + return await this.model.create(payload) } updateItem (query = {}, data, opt, populate = []) { diff --git a/app/service/tag.js b/app/service/tag.js index 7ff9385..2932d8b 100644 --- a/app/service/tag.js +++ b/app/service/tag.js @@ -15,7 +15,7 @@ module.exports = class TagService extends ProxyService { }, opt) const categories = await this.model.find(query, select, opt).exec() if (categories.length) { - const PUBLISH = this.app.config.modelValidate.article.state.optional.PUBLISH + const PUBLISH = this.app.config.modelEnum.article.state.optional.PUBLISH await Promise.all( categories.map(async item => { const articles = await this.service.article.getList({ diff --git a/app/service/user.js b/app/service/user.js index 09c1fb9..93084f7 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -34,6 +34,7 @@ module.exports = class UserService extends ProxyService { */ async checkCommentAuthor (author) { let user = null + let error = '' const { isObjectId, isObject } = this.app.utils.validate if (isObjectId(author)) { user = await this.getItemById(author) @@ -56,17 +57,31 @@ module.exports = class UserService extends ProxyService { user = await this.updateItemById(id, update) if (user) { this.logger.info('用户更新成功:' + user.name) + this.service.notification.recordUser(user, 'update') } } } } else { - // 创建 - user = await this.create(Object.assign(update, { - role: this.config.modelValidate.user.role.optional.NORMAL - })) + user = await this.getItem({ name: author.name }) + if (user) { + // 名称重复,不能创建评论 + user = null + error = '用户名重复,请修改后再提交' + } else { + // 可以创建 + user = await this.create(Object.assign(update, { + role: this.config.modelEnum.user.role.optional.NORMAL + })) + if (user) { + this.service.notification.recordUser(user, 'create') + } + } } } - return user + if (!user && !error) { + error = '用户不存在' + } + return { user, error } } /** diff --git a/config/config.default.js b/config/config.default.js index 1f2613a..f6c3440 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -101,7 +101,7 @@ module.exports = appInfo => { 500: '服务器错误' } - config.modelValidate = { + config.modelEnum = { article: { // 文章状态 ( 0 草稿(默认) | 1 已发布 ) state: { @@ -110,6 +110,14 @@ module.exports = appInfo => { DRAFT: 0, PUBLISH: 1 } + }, + // 来源 0 原创 | 1 转载 + source: { + default: 0, + optional: { + ORIGINAL: 0, + REPRINT: 1 + } } }, user: { @@ -153,24 +161,27 @@ module.exports = appInfo => { }, classify: { optional: { + // 遵循 type_model_action 模式 // type === 0,系统通知 // todo // type === 1,评论通知 - COMMENT_COMMENT_1: 'comment_comment', // 评论(非回复) - COMMENT_COMMENT_REPLY_1: 'comment_comment_reply', // 评论回复 - COMMENT_COMMENT_UPDATE_1: 'comment_update', // 评论更新(保留) - COMMENT_MESSAGE_1: 'comment_message', // 站内留言 - COMMENT_MESSAGE_REPLY_1: 'comment_message_reply', // 站内留言回复 - COMMENT_MESSAGE_UPDATE_1: 'comment_message_reply', // 站内留言更新 + COMMENT_COMMENT_COMMENT: 'comment_comment', // 评论(非回复) + COMMENT_COMMENT_COMMENT_REPLY: 'comment_comment_reply', // 评论回复 + COMMENT_COMMENT_COMMENT_UPDATE: 'comment_comment_update', // 评论更新(保留) + COMMENT_COMMENT_MESSAGE: 'comment_message', // 站内留言 + COMMENT_COMMENT_MESSAGE_REPLY: 'comment_message_reply', // 站内留言回复 + COMMENT_COMMENT_MESSAGE_UPDATE: 'comment_message_reply', // 站内留言更新 // type === 2,点赞通知 - ARTICLE_LIKE_2: 'article_like', // 文章点赞 - ARTICLE_UNLIKE_2: 'article_unlike', // 文章取消点赞 - COMMENT_LIKE_2: 'coment_like', // 评论点赞 - COMMENT_UNLIKE_2: 'comment_unlike', // 评论取消点赞 + LIKE_ARTICLE_LIKE: 'article_like', // 文章点赞 + LIKE_ARTICLE_UNLIKE: 'article_unlike', // 文章取消点赞 + LIKE_COMMENT_COMMENT_LIKE: 'coment_like', // 评论点赞 + LIKE_COMMENT_MESSAGE_LIKE: 'message_like', // 留言点赞 + LIKE_COMMENT_MESSAGE_UNLIKE: 'message_unlike', // 留言取消点赞 + LIKE_COMMENT_COMMENT_UNLIKE: 'comment_unlike', // 评论取消点赞 // type === 3, 用户操作通知 - USER_MUTE_AUTO_3: 'user_mute_auto', // 用户被自动禁言 - USER_CREATE_3: 'user_create', // 用户创建 - USER_UPDATE_3: 'user_update' // 用户更新 + USER_USER_MUTE_AUTO: 'user_mute_auto', // 用户被自动禁言 + USER_USER_CREATE: 'user_create', // 用户创建 + USER_USER_UPDATE: 'user_update' // 用户更新 } } } From 5c71ec5c596147dea7be66be2b2853dd23adb1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 31 Aug 2018 19:41:58 +0800 Subject: [PATCH 127/208] chore: rename some function --- app.js | 4 ++-- app/controller/setting.js | 2 -- app/service/setting.js | 10 ++++++++-- app/utils/gravatar.js | 2 +- package.json | 1 - 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app.js b/app.js index 8f64112..f55fb57 100644 --- a/app.js +++ b/app.js @@ -5,7 +5,7 @@ const path = require('path') module.exports = app => { app.loader.loadToApp(path.join(app.config.baseDir, 'app/utils'), 'utils') addValidateRule(app) - setSessionstore(app) + mountSessionstoreToApp(app) app.beforeStart(async () => { const ctx = app.createAnonymousContext() await ctx.service.setting.seed() @@ -35,7 +35,7 @@ function addValidateRule (app) { }) } -function setSessionstore (app) { +function mountSessionstoreToApp (app) { app.sessionStore = class Store { constructor (app) { this.app = app diff --git a/app/controller/setting.js b/app/controller/setting.js index 4a91885..99e9cc5 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -43,8 +43,6 @@ module.exports = class SettingController extends Controller { await this.service.setting.updateItemById(exist._id, body) // 抓取友链 const data = await this.service.setting.updateLinks() - // 更新配置后需要挂载到app上 - await this.service.setting.mountToApp() data ? ctx.success(data, '配置更新成功') : ctx.fail('配置更新失败') diff --git a/app/service/setting.js b/app/service/setting.js index 9c4512a..465db80 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -66,14 +66,20 @@ module.exports = class SettingService extends ProxyService { } }) this.logger.info('友链更新成功') + // 更新后挂载到app上 + this.mountToApp(setting) return setting } /** * @desc 把配置挂载到app上 + * @param {Setting} setting 配置 */ - async mountToApp () { - const setting = await this.getItem() + async mountToApp (setting) { + if (!setting) { + setting = await this.getItem() + } this.app.setting = setting || null + this.logger.info('配置挂载App成功') } } diff --git a/app/utils/gravatar.js b/app/utils/gravatar.js index 53c1e21..81d05f2 100644 --- a/app/utils/gravatar.js +++ b/app/utils/gravatar.js @@ -16,6 +16,6 @@ module.exports = app => { d: 'retro', protocol }, opt)) - return url.replace(`${protocol}://`, `${app.config.author.url}/proxy/`) + return url && url.replace(`${protocol}://`, `${app.config.author.url}/proxy/`) || app.config.defaultAvatar } } diff --git a/package.json b/package.json index e56c9ab..fc7a945 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "private": true, "dependencies": { "akismet-api": "^4.2.0", - "axios": "^0.18.0", "bcryptjs": "^2.4.3", "egg": "^2.2.1", "egg-console": "^2.0.1", From b7e319e985774a81b89a06494dff86eff95b501d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 31 Aug 2018 20:06:31 +0800 Subject: [PATCH 128/208] fix: remove promise catch event --- app/lib/plugin/egg-akismet/lib/akismet.js | 4 +--- app/service/article.js | 20 +++++--------------- app/service/comment.js | 2 +- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/app/lib/plugin/egg-akismet/lib/akismet.js b/app/lib/plugin/egg-akismet/lib/akismet.js index 5276fb2..f7c87bf 100644 --- a/app/lib/plugin/egg-akismet/lib/akismet.js +++ b/app/lib/plugin/egg-akismet/lib/akismet.js @@ -39,9 +39,7 @@ class AkismetClient { let valid = true let error = '' if (!isValidKey) { - const v = await this.client.verifyKey().catch(err => { - error = 'Apikey验证失败,错误:' + err.message - }) + const v = await this.client.verifyKey() valid = v if (v) { isValidKey = true diff --git a/app/service/article.js b/app/service/article.js index b526dce..4b31394 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -109,10 +109,7 @@ module.exports = class ArticleService extends ProxyService { path: 'category', select: 'name description' } - ).catch(err => { - this.logger.error('相关文章查询失败,错误:' + err.message) - return null - }) + ) return articles && articles.slice(0, this.app.setting.limit.relatedArticleCount) || null } @@ -138,10 +135,7 @@ module.exports = class ArticleService extends ProxyService { path: 'category', select: 'name description' } - ).catch(err => { - this.logger.error('前一篇文章获取失败,错误:' + err.message) - return null - }) + ) query.createdAt = { $gt: data.createdAt } @@ -155,10 +149,7 @@ module.exports = class ArticleService extends ProxyService { path: 'category', select: 'name description' } - ).catch(err => { - this.logger.error('后一篇文章获取失败,错误:' + err.message) - return null - }) + ) return { prev: prev || null, next: next || null @@ -177,10 +168,9 @@ module.exports = class ArticleService extends ProxyService { { $match: { article: { $in: articleIds } } }, { $group: { _id: '$article', total_count: { $sum: 1 } } } ]) - Promise.all( + await Promise.all( counts.map(count => this.updateItemById(count._id, { $set: { 'meta.comments': count.total_count } })) ) - .then(() => this.logger.info('文章评论数量更新成功')) - .catch(err => this.logger.error('文章评论数量更新失败,错误:' + err.message)) + this.logger.info('文章评论数量更新成功') } } diff --git a/app/service/comment.js b/app/service/comment.js index d21dd77..ca037da 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -76,7 +76,7 @@ module.exports = class CommentService extends ProxyService { // 非回复管理员,非回复自身,才发送给被评论者 if (forwardAuthorId && forwardAuthorId !== authorId && forwardAuthorId !== adminId) { - const forwardAuthor = await this.service.user.getItemById(forwardAuthorId).catch(() => null) + const forwardAuthor = await this.service.user.getItemById(forwardAuthorId) if (forwardAuthor && forwardAuthor.email) { this.service.mail.send(typeTitle, { to: forwardAuthor.email, From aa458f6d7088f2d1deec5a861348d26beb0318f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 31 Aug 2018 21:00:18 +0800 Subject: [PATCH 129/208] feature: sentry support --- app/controller/auth.js | 5 +++++ app/middleware/auth.js | 1 - app/router.js | 1 - app/service/sentry.js | 35 +++++++++++++++++++++++++++++++++++ config/config.default.js | 11 +++++------ config/config.prod.js | 11 +++++++++++ config/plugin.prod.js | 6 ++++++ package.json | 1 + 8 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 app/service/sentry.js create mode 100644 config/plugin.prod.js diff --git a/app/controller/auth.js b/app/controller/auth.js index 9537522..e76249d 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -32,6 +32,9 @@ module.exports = class AuthController extends Controller { async login () { const { ctx } = this + if (ctx.session._isAuthed) { + return ctx.fail('你已登录,请勿重复登录') + } const body = this.ctx.validateBody(this.rules.login) const user = await this.service.user.getItem({ name: body.username }) if (!user) { @@ -42,6 +45,8 @@ module.exports = class AuthController extends Controller { return ctx.fail('密码错误') } const token = this.service.auth.setCookie(user, true) + // 调用 rotateCsrfSecret 刷新用户的 CSRF token + ctx.rotateCsrfSecret() this.logger.info(`用户登录成功, ID:${user._id},用户名:${user.name}`) ctx.success({ id: user._id, token }, '登录成功') } diff --git a/app/middleware/auth.js b/app/middleware/auth.js index 11a2fee..363e88c 100644 --- a/app/middleware/auth.js +++ b/app/middleware/auth.js @@ -20,7 +20,6 @@ module.exports = app => { return ctx.fail(401, '用户不存在') } ctx.session._user = user - ctx.session._isAdmin = user.role === app.config.modelEnum.user.role.optional.ADMIN ctx.session._isAuthed = true await next() } diff --git a/app/router.js b/app/router.js index efcbba6..0519338 100644 --- a/app/router.js +++ b/app/router.js @@ -14,7 +14,6 @@ module.exports = app => { require('./router/backend')(app) require('./router/frontend')(app) - router.all('*', ctx => { const code = 404 ctx.fail(code, app.config.codeMap[code]) diff --git a/app/service/sentry.js b/app/service/sentry.js new file mode 100644 index 0000000..0f747ed --- /dev/null +++ b/app/service/sentry.js @@ -0,0 +1,35 @@ +const { Service } = require('egg') + +module.exports = class SentryService extends Service { + /** + * filter errors need to be submitted to sentry + * + * @param {any} err error + * @return {boolean} true for submit, default true + * @memberof SentryService + */ + judgeError (err) { + // ignore HTTP Error + return !(err.status && err.status >= 500) + } + + // user information + get user () { + return this.app._admin + } + + get extra () { + return { + ip: this.ctx.ip, + payload: this.ctx.request.body, + query: this.ctx.query, + params: this.ctx.params + } + } + + get tags () { + return { + url: this.ctx.request.url + } + } +} diff --git a/config/config.default.js b/config/config.default.js index f6c3440..4b2dc62 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -21,7 +21,7 @@ module.exports = appInfo => { ] config.session = { - key: appInfo.name + '-token', + key: appInfo.name + '_token', maxAge: 60000 * 60 * 24 * 7, signed: true } @@ -195,11 +195,10 @@ module.exports = appInfo => { config.defaultAvatar = 'https://static.jooger.me/img/common/avatar.png' - // 限制参数 - config.limit = { - relatedArticleLimit: 10, - commentSpamLimit: 3, - hotLimit: 7 + config.onerror = { + js (err, ctx) { + + } } return config diff --git a/config/config.prod.js b/config/config.prod.js index 164ac18..0948f5c 100644 --- a/config/config.prod.js +++ b/config/config.prod.js @@ -1,10 +1,21 @@ module.exports = () => { const config = exports = {} + config.security = { + csrf: { + headerName: 'x-csrf-token', + cookieName: 'csrfToken' + } + } + config.console = { debug: false, error: false } + config.sentry = { + dsn: 'https://43ea4130c7684fb3aa86404172cf67a1@sentry.io/1272403' + } + return config } diff --git a/config/plugin.prod.js b/config/plugin.prod.js new file mode 100644 index 0000000..f134740 --- /dev/null +++ b/config/plugin.prod.js @@ -0,0 +1,6 @@ +'use strict' + +exports.sentry = { + enable: true, + package: 'egg-sentry', +} diff --git a/package.json b/package.json index fc7a945..c1fac7b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "egg-redis": "^2.0.0", "egg-router-plus": "^1.2.2", "egg-scripts": "^2.5.0", + "egg-sentry": "^1.0.0", "egg-validate": "^1.1.1", "geoip-lite": "^1.3.2", "gravatar": "^1.6.0", From 3583c31876ea2b5e708e64e6ee240ab694860aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 31 Aug 2018 21:09:22 +0800 Subject: [PATCH 130/208] chore: change server listen port to 3002 --- config/config.default.js | 7 +++++++ package.json | 1 + 2 files changed, 8 insertions(+) diff --git a/config/config.default.js b/config/config.default.js index 4b2dc62..9c4ee94 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -1,6 +1,13 @@ module.exports = appInfo => { const config = exports = {} + config.cluster = { + listen: { + port: 3002, + hostname: '127.0.0.1' + } + } + // use for cookie sign key, should change to your own and keep security config.keys = appInfo.name + '_1534765762288_2697' diff --git a/package.json b/package.json index c1fac7b..5ba2cbd 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "egg": "^2.2.1", "egg-console": "^2.0.1", "egg-mongoose": "^3.1.0", + "egg-oss": "^1.1.0", "egg-redis": "^2.0.0", "egg-router-plus": "^1.2.2", "egg-scripts": "^2.5.0", From 59ff1fe66b30e3e3d3beeb397dc2e530fa0f0305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 2 Sep 2018 02:20:18 +0800 Subject: [PATCH 131/208] fix: fix some validate bug --- app.js | 3 + app/controller/auth.js | 2 +- app/controller/setting.js | 6 +- app/controller/user.js | 18 ++++++ app/lib/plugin/egg-alinode/agent.js | 9 +++ .../egg-alinode/config/config.default.js | 39 +++++++++++++ app/lib/plugin/egg-alinode/lib/alinode.js | 46 +++++++++++++++ app/lib/plugin/egg-alinode/package.json | 5 ++ .../plugin/egg-alinode/schedule/removeLogs.js | 58 +++++++++++++++++++ app/model/setting.js | 5 ++ app/router/backend.js | 1 + config/config.default.js | 6 -- config/plugin.prod.js | 7 +++ package.json | 2 + 14 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 app/lib/plugin/egg-alinode/agent.js create mode 100644 app/lib/plugin/egg-alinode/config/config.default.js create mode 100644 app/lib/plugin/egg-alinode/lib/alinode.js create mode 100644 app/lib/plugin/egg-alinode/package.json create mode 100644 app/lib/plugin/egg-alinode/schedule/removeLogs.js diff --git a/app.js b/app.js index f55fb57..c2987aa 100644 --- a/app.js +++ b/app.js @@ -8,8 +8,11 @@ module.exports = app => { mountSessionstoreToApp(app) app.beforeStart(async () => { const ctx = app.createAnonymousContext() + // 初始化配置(如果有必要) await ctx.service.setting.seed() + // 配置挂载到App上 await ctx.service.setting.mountToApp() + // 初始化管理员(如果有必要) await ctx.service.auth.seed() }) } diff --git a/app/controller/auth.js b/app/controller/auth.js index e76249d..306f38d 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -19,7 +19,7 @@ module.exports = class AuthController extends Controller { site: { type: 'url', required: false }, description: { type: 'string', required: false }, avatar: { type: 'string', required: false }, - l: { type: 'string', required: false }, + slogan: { type: 'string', required: false }, company: { type: 'string', required: false }, location: { type: 'string', required: false } }, diff --git a/app/controller/setting.js b/app/controller/setting.js index 99e9cc5..a3562a3 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -26,7 +26,11 @@ module.exports = class SettingController extends Controller { async index () { const { ctx } = this ctx.validate(this.rules.index, ctx.query) - const data = await this.service.setting.getItem() + let select = null + if (ctx.query.filter) { + select = ctx.query.filter + } + const data = await this.service.setting.getItem({}, select) data ? ctx.success(data, '配置获取成功') : ctx.fail('配置获取失败') diff --git a/app/controller/user.js b/app/controller/user.js index 23ae17f..6a7a697 100644 --- a/app/controller/user.js +++ b/app/controller/user.js @@ -5,6 +5,14 @@ const { Controller } = require('egg') module.exports = class UserController extends Controller { + get rules () { + return { + update: { + mute: { type: 'boolean', required: false } + } + } + } + async list () { const { ctx } = this let select = '-password' @@ -29,4 +37,14 @@ module.exports = class UserController extends Controller { ? ctx.success(data, '用户详情获取成功') : ctx.fail('用户详情获取失败') } + + async update () { + const { ctx } = this + const { id } = ctx.validateParamsObjectId() + const body = this.ctx.validateBody(this.rules.update) + const data = await this.service.user.updateItemById(id, body, '-password') + data + ? ctx.success(data, '用户更新成功') + : ctx.fail('用户更新失败') + } } diff --git a/app/lib/plugin/egg-alinode/agent.js b/app/lib/plugin/egg-alinode/agent.js new file mode 100644 index 0000000..e03a554 --- /dev/null +++ b/app/lib/plugin/egg-alinode/agent.js @@ -0,0 +1,9 @@ +module.exports = agent => { + agent.logger.info(333) + agent.messenger.on('egg-ready', () => { + agent.logger.info(222) + agent.messenger.on('alinode-run', config => { + agent.logger.info(111) + }) + }) +} diff --git a/app/lib/plugin/egg-alinode/config/config.default.js b/app/lib/plugin/egg-alinode/config/config.default.js new file mode 100644 index 0000000..cb0cd7d --- /dev/null +++ b/app/lib/plugin/egg-alinode/config/config.default.js @@ -0,0 +1,39 @@ +'use strict' + +const path = require('path') +const mkdirp = require('mkdirp') + +module.exports = appInfo => { + const exports = {} + + const appRoot = appInfo.env === 'local' || appInfo.env === 'unittest' ? appInfo.baseDir : appInfo.HOME + let alinodeLogdir = path.join(appRoot, 'logs/alinode') + // try to use NODE_LOG_DIR first + if (process.env.NODE_LOG_DIR) { + alinodeLogdir = process.env.NODE_LOG_DIR + } + mkdirp.sync(alinodeLogdir) + + exports.alinode = { + enable: true, + // default is wss://agentserver.node.aliyun.com:8080 + server: 'wss://agentserver.node.aliyun.com:8080', + appid: '', + secret: '', + cmddir: path.dirname(require.resolve('commandx/package.json')), + logdir: alinodeLogdir, + error_log: [ + path.join(appRoot, `logs/${appInfo.pkg.name}/common-error.log`), + path.join(appRoot, 'logs/stderr.log'), + ], + packages: [ + path.join(appInfo.baseDir, 'package.json'), + ], + // seconds + reconnectDelay: 10, + heartbeatInterval: 60, + reportInterval: 60, + } + + return exports +} diff --git a/app/lib/plugin/egg-alinode/lib/alinode.js b/app/lib/plugin/egg-alinode/lib/alinode.js new file mode 100644 index 0000000..847c84c --- /dev/null +++ b/app/lib/plugin/egg-alinode/lib/alinode.js @@ -0,0 +1,46 @@ +const assert = require('assert') +const AlinodeAgent = require('agentx') +const homedir = require('node-homedir') +const fs = require('fs') +const path = require('path') + +module.exports = agent => { + agent.addSingleton('alinode', createClient) +} + +function createClient (config, agent) { + if (!config.enable) { + agent.coreLogger.info('[egg-alinode] disable') + return + } + assert(config.appid, 'config.alinode.appid required') + assert(config.secret, 'config.alinode.secret required') + + const nodepathFile = path.join(homedir(), '.nodepath') + const nodeBin = path.dirname(process.execPath) + fs.writeFileSync(nodepathFile, nodeBin) + config.logger = agent.coreLogger + config.libMode = true + const client = new AlinodeAgent(config) + agent.beforeStart(async () => { + agent.coreLogger.info('[egg-alinode] alinode agentx started, node versions: %j, update %s with %j, config: %j', + process.versions, + nodepathFile, + nodeBin, { + server: config.server, + appid: config.appid, + } + ) + }) + return { + client, + config, + run () { + return this.client.run() + }, + restart (config) { + this.client = new AlinodeAgent(config || this.config) + this.config = config + } + } +} diff --git a/app/lib/plugin/egg-alinode/package.json b/app/lib/plugin/egg-alinode/package.json new file mode 100644 index 0000000..d3e3553 --- /dev/null +++ b/app/lib/plugin/egg-alinode/package.json @@ -0,0 +1,5 @@ +{ + "eggPlugin": { + "name": "alinode" + } +} diff --git a/app/lib/plugin/egg-alinode/schedule/removeLogs.js b/app/lib/plugin/egg-alinode/schedule/removeLogs.js new file mode 100644 index 0000000..233815a --- /dev/null +++ b/app/lib/plugin/egg-alinode/schedule/removeLogs.js @@ -0,0 +1,58 @@ +'use strict' + +const path = require('path') +const fs = require('mz/fs') +const moment = require('moment') + +module.exports = app => { + const exports = {} + + const logger = app.coreLogger + + exports.schedule = { + type: 'worker', // only one worker run this task + cron: '0 0 * * *', // run every day at 00:00 + } + exports.task = function* () { + const logdir = app.config.alinode.logdir + const maxDays = 7 + try { + yield removeExpiredLogFiles(logdir, maxDays) + } catch (err) { + logger.error(err) + } + } + + // remove expired log files: [access|node]-YYYYMMDD.log + function* removeExpiredLogFiles (logdir, maxDays) { + const files = yield fs.readdir(logdir) + const expriedDate = moment().subtract(maxDays, 'days').startOf('date') + const names = files.filter(name => { + const m = /^(?:access|node)\-(\d{8})\.log$/.exec(name) + if (!m) { + return false + } + const date = moment(m[1], 'YYYYMMDD').startOf('date') + if (!date.isValid()) { + return false + } + return date.isBefore(expriedDate) + }) + if (names.length === 0) { + return + } + + logger.info(`[egg-alinode] start remove ${logdir} files: ${names.join(', ')}`) + yield names.map(name => function* () { + const logfile = path.join(logdir, name) + try { + yield fs.unlink(logfile) + } catch (err) { + err.message = `[egg-alinode] remove logfile ${logfile} error, ${err.message}` + logger.error(err) + } + }) + } + + return exports +} diff --git a/app/model/setting.js b/app/model/setting.js index a6c555c..3ddf3d9 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -34,6 +34,11 @@ module.exports = app => { bucket: { type: String, default: '' }, region: { type: String, default: '' } }, + // 阿里node平台 + alinode: { + appid: { type: String, default: '' }, + secret: { type: String, default: '' } + }, // 163邮箱 mail: { user: { type: String, default: '' }, diff --git a/app/router/backend.js b/app/router/backend.js index 8294c84..68ddbb6 100644 --- a/app/router/backend.js +++ b/app/router/backend.js @@ -39,6 +39,7 @@ module.exports = app => { // User backendRouter.get('/users', auth, controller.user.list) backendRouter.get('/users/:id', auth, controller.user.item) + backendRouter.patch('/users/:id', auth, controller.user.update) // Setting backendRouter.get('/setting', auth, controller.setting.index) diff --git a/config/config.default.js b/config/config.default.js index 9c4ee94..d9ac9f7 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -202,11 +202,5 @@ module.exports = appInfo => { config.defaultAvatar = 'https://static.jooger.me/img/common/avatar.png' - config.onerror = { - js (err, ctx) { - - } - } - return config } diff --git a/config/plugin.prod.js b/config/plugin.prod.js index f134740..ff1fa86 100644 --- a/config/plugin.prod.js +++ b/config/plugin.prod.js @@ -1,6 +1,13 @@ 'use strict' +// const path = require('path') + exports.sentry = { enable: true, package: 'egg-sentry', } + +// exports.alinode = { +// enable: true, +// path: path.join(__dirname, '../app/lib/plugin/egg-alinode') +// } diff --git a/package.json b/package.json index 5ba2cbd..4d753b2 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "", "private": true, "dependencies": { + "agentx": "^1.9.11", "akismet-api": "^4.2.0", "bcryptjs": "^2.4.3", "egg": "^2.2.1", @@ -25,6 +26,7 @@ "marked": "^0.5.0", "mongoose": "5.2.8", "mongoose-paginate-v2": "^1.0.12", + "node-homedir": "^1.1.1", "nodemailer": "^4.6.8", "validator": "^10.6.0", "zlib": "^1.0.5" From 658a365885cc3a59376ed69893d63f17c9237311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 2 Sep 2018 21:46:22 +0800 Subject: [PATCH 132/208] feature: notification add count controller --- app/controller/auth.js | 5 ++++- app/controller/notification.js | 6 ++++++ app/middleware/auth.js | 6 ++---- app/router/backend.js | 1 + app/service/proxy.js | 8 ++++++-- config/config.prod.js | 4 ++++ 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/controller/auth.js b/app/controller/auth.js index 306f38d..1d5e43a 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -59,7 +59,10 @@ module.exports = class AuthController extends Controller { } async info () { - this.ctx.success(this.ctx.session._user, '管理员信息获取成功') + this.ctx.success({ + info: this.ctx.session._user, + token: this.ctx.session._token + }, '管理员信息获取成功') } /** diff --git a/app/controller/notification.js b/app/controller/notification.js index 6442afa..fed772d 100644 --- a/app/controller/notification.js +++ b/app/controller/notification.js @@ -64,6 +64,12 @@ module.exports = class NotificationController extends Controller { : ctx.fail('通告列表获取失败') } + async count () { + const { ctx } = this + const count = await this.service.notification.count({ viewed: false }) + ctx.success(count, '未读通告数量获取成功') + } + async view () { const { ctx } = this const params = ctx.validateParamsObjectId() diff --git a/app/middleware/auth.js b/app/middleware/auth.js index 363e88c..c059b1c 100644 --- a/app/middleware/auth.js +++ b/app/middleware/auth.js @@ -12,9 +12,7 @@ module.exports = app => { if (!ctx.session._verify) { return ctx.fail(401) } - const userId = ctx.cookies.get(app.config.userCookieKey, { - signed: false - }) + const userId = ctx.cookies.get(app.config.userCookieKey, app.config.session.signed) const user = await ctx.service.user.getItemById(userId, '-password') if (!user) { return ctx.fail(401, '用户不存在') @@ -31,7 +29,7 @@ function verifyToken (app) { const { config, logger } = app return async (ctx, next) => { ctx.session._verify = false - const token = ctx.cookies.get(config.session.key) + const token = ctx.cookies.get(config.session.key, app.config.session.signed) if (token) { let decodedToken = null try { diff --git a/app/router/backend.js b/app/router/backend.js index 68ddbb6..bb16935 100644 --- a/app/router/backend.js +++ b/app/router/backend.js @@ -54,6 +54,7 @@ module.exports = app => { // Notification backendRouter.get('/notifications', auth, controller.notification.list) + backendRouter.get('/notifications/count', auth, controller.notification.count) backendRouter.patch('/notifications/view', auth, controller.notification.viewAll) backendRouter.patch('/notifications/:id/view', auth, controller.notification.view) backendRouter.delete('/notifications/:id', auth, controller.notification.delete) diff --git a/app/service/proxy.js b/app/service/proxy.js index 50fdf60..c99f834 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -47,8 +47,8 @@ module.exports = class ProxyService extends Service { return Q.exec() } - async create (payload) { - return await this.model.create(payload) + create (payload) { + return this.model.create(payload) } updateItem (query = {}, data, opt, populate = []) { @@ -90,4 +90,8 @@ module.exports = class ProxyService extends Service { aggregate (pipeline = []) { return this.model.aggregate(pipeline) } + + count (filter) { + return this.model.count(filter).exec() + } } diff --git a/config/config.prod.js b/config/config.prod.js index 0948f5c..32f1b0e 100644 --- a/config/config.prod.js +++ b/config/config.prod.js @@ -8,6 +8,10 @@ module.exports = () => { } } + config.session = { + domain: '.jooger.me' + } + config.console = { debug: false, error: false From 95635eeafbdb0722a7db37ef856e95d140bed714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 3 Sep 2018 20:22:06 +0800 Subject: [PATCH 133/208] update: article create params validate fix --- app/controller/article.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controller/article.js b/app/controller/article.js index 47406fa..1dcdd16 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -26,10 +26,10 @@ module.exports = class ArticleController extends Controller { content: { type: 'string', required: true }, description: { type: 'string', required: false }, keywords: { type: 'array', required: false }, - category: { type: 'objectId', required: false }, + category: { type: 'objectId', required: true }, tag: { type: 'array', required: false, itemType: 'objectId' }, - state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: false }, - source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: false }, + state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: true }, + source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: true }, thumb: { type: 'url', required: false }, createdAt: { type: 'dateTime', required: false } }, From 9bbab9da29f0e41aef27537be6adfc2d57a6c3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 3 Sep 2018 21:52:58 +0800 Subject: [PATCH 134/208] update: change akismet package --- app/lib/plugin/egg-akismet/lib/akismet.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/plugin/egg-akismet/lib/akismet.js b/app/lib/plugin/egg-akismet/lib/akismet.js index f7c87bf..39e98ae 100644 --- a/app/lib/plugin/egg-akismet/lib/akismet.js +++ b/app/lib/plugin/egg-akismet/lib/akismet.js @@ -1,4 +1,4 @@ -const akismet = require('akismet-api') +const akismet = require('akismet') module.exports = app => { app.addSingleton('akismet', createClient) diff --git a/package.json b/package.json index 4d753b2..df399a4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "dependencies": { "agentx": "^1.9.11", - "akismet-api": "^4.2.0", + "akismet": "^1.0.0", "bcryptjs": "^2.4.3", "egg": "^2.2.1", "egg-console": "^2.0.1", From d2bf5d793cf97b49a8cc3acf2052c7ae1a7b4276 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 4 Sep 2018 01:50:57 +0800 Subject: [PATCH 135/208] update: akismet support service --- app/controller/article.js | 6 +- app/controller/comment.js | 4 +- app/lib/plugin/egg-akismet/lib/akismet.js | 122 +++------------------- app/lib/plugin/egg-mailer/lib/mailer.js | 12 ++- app/middleware/headers.js | 2 - app/service/akismet.js | 74 +++++++++++++ app/service/mail.js | 9 +- app/service/notification.js | 16 ++- app/service/proxy.js | 4 + app/service/setting.js | 2 +- config/config.default.js | 10 +- config/config.local.js | 4 + config/plugin.js | 5 + package.json | 3 +- 14 files changed, 145 insertions(+), 128 deletions(-) create mode 100644 app/service/akismet.js diff --git a/app/controller/article.js b/app/controller/article.js index 1dcdd16..f168f96 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -58,7 +58,7 @@ module.exports = class ArticleController extends Controller { } }) ctx.validate(this.rules.list, ctx.query) - const { page, limit, state, keyword, category, tag, order, sortBy, startDate, endDate } = ctx.query + const { page, limit, state, keyword, category, tag, source, order, sortBy, startDate, endDate } = ctx.query const options = { sort: { updatedAt: -1, @@ -77,7 +77,7 @@ module.exports = class ArticleController extends Controller { } ] } - const query = { state, category, tag } + const query = { state, category, tag, source } // 搜索关键词 if (keyword) { @@ -116,6 +116,8 @@ module.exports = class ArticleController extends Controller { } } } + console.log(query) + const data = await this.service.article.getLimitListByQuery(ctx.processPayload(query), options) data ? ctx.success(data, '文章列表获取成功') diff --git a/app/controller/comment.js b/app/controller/comment.js index 53fbffe..5e0025f 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -187,7 +187,7 @@ module.exports = class CommentController extends Controller { } // 永链 const permalink = this.service.comment.getPermalink(body) - const isSpam = await this.app.akismet.checkSpam({ + const isSpam = await this.service.akismet.checkSpam({ user_ip: ip, user_agent: meta.ua, referrer: meta.referer, @@ -252,7 +252,7 @@ module.exports = class CommentController extends Controller { comment_content: exist.content, is_test: !this.config.isProd } - const isSpam = await this.app.akismet.checkSpam(opt) + const isSpam = await this.service.akismet.checkSpam(opt) // 如果是Spam评论 if (isSpam) { this.logger.warn('检测为垃圾评论,禁止发布') diff --git a/app/lib/plugin/egg-akismet/lib/akismet.js b/app/lib/plugin/egg-akismet/lib/akismet.js index 39e98ae..5ee2ff5 100644 --- a/app/lib/plugin/egg-akismet/lib/akismet.js +++ b/app/lib/plugin/egg-akismet/lib/akismet.js @@ -1,118 +1,22 @@ -const akismet = require('akismet') +const akismet = require('akismet-api') module.exports = app => { app.addSingleton('akismet', createClient) app.beforeStart(async () => { - const { valid, error } = await app.akismet.verifyKey() - if (valid) { - app.coreLogger.info('[egg-akismet] 服务启动成功') - } else { - app.coreLogger.error(`[egg-akismet] 服务启动失败:${error}`) - } - }) -} - -function createClient (config, app) { - return new AkismetClient(config, app) -} - -// Akismet apikey是否验证通过 -let isValidKey = false - -/** - * @desc Akismet Client Class - * @param {String} key Akismet apikey - * @param {String} blog Akismet blog - */ -class AkismetClient { - constructor (config, app) { - this.config = config - this.app = app - this.init() - } - - init () { - this.client = akismet.client(this.config) - } - - async verifyKey () { - let valid = true - let error = '' - if (!isValidKey) { - const v = await this.client.verifyKey() - valid = v - if (v) { - isValidKey = true + try { + const valid = await app.akismet.verifyKey() + if (valid) { + app.coreLogger.info('[egg-akismet] 服务启动成功') + app._akismetValid = true } else { - error = '无效的Apikey' - this.client = null + app.coreLogger.error('[egg-akismet] 服务启动失败:无效的Apikey') } + } catch (error) { + app.coreLogger.error('[egg-akismet] ' + error.message) } - return { valid, error } - } - - // 检测是否是spam - checkSpam (opt = {}) { - this.app.coreLogger.info('验证评论中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.checkSpam(opt, (err, spam) => { - if (err) { - this.app.coreLogger.error('[egg-akismet] 评论验证失败,将跳过Spam验证,错误:', err.message) - return reject(false) - } - if (spam) { - this.app.coreLogger.warn('[egg-akismet] 评论验证不通过,疑似垃圾评论') - resolve(true) - } else { - this.app.coreLogger.info('[egg-akismet] 评论验证通过') - resolve(false) - } - }) - } else { - this.app.coreLogger.warn('[egg-akismet] Apikey未认证,将跳过Spam验证') - resolve(false) - } - }) - } - - // 提交被误检为spam的正常评论 - submitSpam (opt = {}) { - this.app.coreLogger.info('[egg-akismet] 误检Spam垃圾评论报告提交中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.submitSpam(opt, err => { - if (err) { - this.app.coreLogger.error('[egg-akismet] 误检Spam垃圾评论报告提交失败') - return reject(err) - } - this.app.coreLogger.info('[egg-akismet] 误检Spam垃圾评论报告提交成功') - resolve() - }) - } else { - this.app.coreLogger.warn('[egg-akismet] Apikey未认证,误检Spam垃圾评论报告提交失败') - resolve() - } - }) - } + }) +} - // 提交被误检为正常评论的spam - submitHam (opt = {}) { - this.app.coreLogger.info('[egg-akismet] 误检正常评论报告提交中...') - return new Promise((resolve, reject) => { - if (isValidKey) { - this.client.submitSpam(opt, err => { - if (err) { - this.app.coreLogger.error('[egg-akismet] 误检正常评论报告提交失败') - return reject(err) - } - this.app.coreLogger.info('[egg-akismet] 误检正常评论报告提交成功') - resolve() - }) - } else { - this.app.coreLogger.warn('[egg-akismet] Apikey未认证,误检正常评论报告提交失败') - resolve() - } - }) - } +function createClient (config) { + return akismet.client(config) } diff --git a/app/lib/plugin/egg-mailer/lib/mailer.js b/app/lib/plugin/egg-mailer/lib/mailer.js index 672493d..5256838 100644 --- a/app/lib/plugin/egg-mailer/lib/mailer.js +++ b/app/lib/plugin/egg-mailer/lib/mailer.js @@ -4,11 +4,18 @@ module.exports = app => { app.addSingleton('mailer', createClient) } -function createClient (config) { +function createClient (config, app) { return { client: null, getClient (opt) { - return this.client || (this.client = nodemailer.createTransport(Object.assign({}, config, opt))) + if (!this.client) { + try { + this.client = nodemailer.createTransport(Object.assign({}, config, opt)) + } catch (err) { + app.coreLogger.error('[egg-mailer] 邮件客户端初始化失败,错误:' + err.message) + } + } + return this.client }, async verify () { await new Promise((resolve, reject) => { @@ -17,6 +24,7 @@ function createClient (config) { } this.client.verify(err => { if (err) { + app.coreLogger.error('[egg-mailer] ' + err.message) reject(err) } else { resolve() diff --git a/app/middleware/headers.js b/app/middleware/headers.js index bafc56c..821d822 100644 --- a/app/middleware/headers.js +++ b/app/middleware/headers.js @@ -15,8 +15,6 @@ module.exports = (opt, app) => { response.set('Access-Control-Allow-Origin', origin) } response.set('Access-Control-Allow-Headers', 'token, Authorization, Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With') - response.set('Access-Control-Allow-Methods', 'PUT,PATCH,POST,GET,DELETE,OPTIONS') - response.set('Access-Control-Allow-Credentials', true) response.set('Content-Type', 'application/json;charset=utf-8') response.set('X-Powered-By', `${app.config.name}/${app.config.version}`) diff --git a/app/service/akismet.js b/app/service/akismet.js new file mode 100644 index 0000000..093f4e6 --- /dev/null +++ b/app/service/akismet.js @@ -0,0 +1,74 @@ +/** + * @desc Akismet Services + */ + +const { Service } = require('egg') + +module.exports = class AkismetService extends Service { + checkSpam (opt = {}) { + this.app.coreLogger.info('验证评论中...') + return new Promise(resolve => { + if (this.app._akismetValid) { + this.app.akismet.checkSpam(opt, (err, spam) => { + if (err) { + this.app.coreLogger.error('评论验证失败,将跳过Spam验证,错误:', err.message) + this.service.notification.recordGeneral('AKISMET', 'CHECK_FAIL', err) + return resolve(true) + } + if (spam) { + this.app.coreLogger.warn('评论验证不通过,疑似垃圾评论') + resolve(true) + } else { + this.app.coreLogger.info('评论验证通过') + resolve(false) + } + }) + } else { + this.app.coreLogger.warn('Apikey未认证,将跳过Spam验证') + resolve(false) + } + }) + } + + // 提交被误检为spam的正常评论 + submitSpam (opt = {}) { + this.app.coreLogger.info('误检Spam垃圾评论报告提交中...') + return new Promise((resolve, reject) => { + if (this.app._akismetValid) { + this.app.akismet.submitSpam(opt, err => { + if (err) { + this.app.coreLogger.error('误检Spam垃圾评论报告提交失败') + this.service.notification.recordGeneral('AKISMET', 'CHECK_FAIL', err) + return reject(err) + } + this.app.coreLogger.info('误检Spam垃圾评论报告提交成功') + resolve() + }) + } else { + this.app.coreLogger.warn('Apikey未认证,误检Spam垃圾评论报告提交失败') + resolve() + } + }) + } + + // 提交被误检为正常评论的spam + submitHam (opt = {}) { + this.app.coreLogger.info('误检正常评论报告提交中...') + return new Promise((resolve, reject) => { + if (this.app._akismetValid) { + this.app.akismet.submitSpam(opt, err => { + if (err) { + this.app.coreLogger.error('误检正常评论报告提交失败') + this.service.notification.recordGeneral('AKISMET', 'CHECK_FAIL', err) + return reject(err) + } + this.app.coreLogger.info('误检正常评论报告提交成功') + resolve() + }) + } else { + this.app.coreLogger.warn('Apikey未认证,误检正常评论报告提交失败') + resolve() + } + }) + } +} diff --git a/app/service/mail.js b/app/service/mail.js index 3c0aeeb..a3354a3 100644 --- a/app/service/mail.js +++ b/app/service/mail.js @@ -15,7 +15,9 @@ module.exports = class MailService extends Service { mailerClient = client = this.app.mailer.getClient({ auth: keys.mail }) - await this.app.mailer.verify() + await this.app.mailer.verify().catch(err => { + this.service.notification.recordGeneral('MAIL', 'VERIFY_FAIL', err) + }) } const opt = Object.assign({ from: `${this.config.author.name} <${keys.mail.user}>` @@ -28,10 +30,11 @@ module.exports = class MailService extends Service { await new Promise((resolve, reject) => { client.sendMail(opt, (err, info) => { if (err) { - this.logger.error(type + toAdmin + '邮件发送失败,TO:' + opt.to + ',错误:' + err.message) + this.logger.error(type + toAdmin + ' 邮件发送失败,TO:' + opt.to + ',错误:' + err.message) + this.service.notification.recordGeneral('MAIL', 'SEND_FAIL', err) return reject(err) } - this.logger.info(type + toAdmin + '邮件发送成功,TO:' + opt.to) + this.logger.info(type + toAdmin + ' 邮件发送成功,TO:' + opt.to) resolve(info) }) }) diff --git a/app/service/notification.js b/app/service/notification.js index 8b200d5..aec965f 100644 --- a/app/service/notification.js +++ b/app/service/notification.js @@ -14,7 +14,7 @@ module.exports = class NotificationService extends ProxyService { } // 记录通告 - async record (typeKey, model, action, target, actors) { + async record (typeKey, model, action, verb, target, actors) { if (!typeKey || !model || !action) return const modelName = this.app.utils.validate.isString(model) ? model @@ -22,7 +22,9 @@ module.exports = class NotificationService extends ProxyService { const type = this.notificationConfig.type.optional[typeKey] const classifyKey = [typeKey, modelName, action].join('_') const classify = this.notificationConfig.classify.optional[classifyKey] - const verb = this.genVerb(classifyKey) + if (!verb) { + verb = this.genVerb(classifyKey) + } const payload = { type, classify, verb, target, actors } const data = await this.create(payload) if (data) { @@ -30,6 +32,10 @@ module.exports = class NotificationService extends ProxyService { } } + async recordGeneral (model, action, err) { + this.record('GENERAL', model, action, err.message || err) + } + // 记录评论相关动作 async recordComment (comment, handle = 'create') { if (!comment || !comment._id) return @@ -64,7 +70,7 @@ module.exports = class NotificationService extends ProxyService { // 更新 action += '_UPDATE' } - this.record('COMMENT', 'COMMENT', action, target, actors) + this.record('COMMENT', 'COMMENT', action, null, target, actors) } recordLike (type, model, user, like = false) { @@ -92,7 +98,7 @@ module.exports = class NotificationService extends ProxyService { } } action += actionSuffix - this.record('LIKE', modelName, action, target, actors) + this.record('LIKE', modelName, action, null, target, actors) } recordUser (user, handle) { @@ -110,7 +116,7 @@ module.exports = class NotificationService extends ProxyService { } else if (handle === 'mute') { action += 'MUTE_AUTO' } - this.record('USER', 'USER', action, target, actors) + this.record('USER', 'USER', action, null, target, actors) } // 获取操作简语 diff --git a/app/service/proxy.js b/app/service/proxy.js index c99f834..5bd716c 100644 --- a/app/service/proxy.js +++ b/app/service/proxy.js @@ -51,6 +51,10 @@ module.exports = class ProxyService extends Service { return this.model.create(payload) } + newAndSave (payload) { + return new this.model(payload).save() + } + updateItem (query = {}, data, opt, populate = []) { opt = this.app.merge({ lean: true, diff --git a/app/service/setting.js b/app/service/setting.js index 465db80..1893664 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -18,7 +18,7 @@ module.exports = class SettingService extends ProxyService { if (exist) { return exist } - const data = await this.create() + const data = await this.newAndSave() if (data) { this.logger.info('Setting初始化成功') } else { diff --git a/config/config.default.js b/config/config.default.js index d9ac9f7..98bff1c 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -27,6 +27,12 @@ module.exports = appInfo => { 'headers' ] + config.cors = { + enable: true, + credentials: true, + allowMethods: 'GET,PUT,POST,DELETE,PATCH,OPTIONS' + } + config.session = { key: appInfo.name + '_token', maxAge: 60000 * 60 * 24 * 7, @@ -170,7 +176,9 @@ module.exports = appInfo => { optional: { // 遵循 type_model_action 模式 // type === 0,系统通知 - // todo + GENERAL_MAIL_VERIFY_FAIL: 'mail_verify_fail', // 邮件客户端校验失败 + GENERAL_MAIL_SEND_FAIL: 'mail_send_fail', // 邮件发送失败 + GENERAL_AKISMET_CHECK_FAIL: 'akismet_check_fail', // akismet检测失败 // type === 1,评论通知 COMMENT_COMMENT_COMMENT: 'comment_comment', // 评论(非回复) COMMENT_COMMENT_COMMENT_REPLY: 'comment_comment_reply', // 评论回复 diff --git a/config/config.local.js b/config/config.local.js index a25719a..cca5e08 100644 --- a/config/config.local.js +++ b/config/config.local.js @@ -9,6 +9,10 @@ module.exports = () => { } config.security = { + domainWhiteList: [ + 'http://localhost:8080', + 'http://127.0.0.1:8080' + ], csrf: { ignore: () => true } diff --git a/config/plugin.js b/config/plugin.js index cb4eb0e..1ee038d 100644 --- a/config/plugin.js +++ b/config/plugin.js @@ -5,6 +5,11 @@ const path = require('path') // had enabled by egg // exports.static = true +exports.cors = { + enable: true, + package: 'egg-cors' +} + exports.mongoose = { enable: true, package: 'egg-mongoose' diff --git a/package.json b/package.json index df399a4..a015cb1 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "private": true, "dependencies": { "agentx": "^1.9.11", - "akismet": "^1.0.0", + "akismet-api": "^4.2.0", "bcryptjs": "^2.4.3", "egg": "^2.2.1", "egg-console": "^2.0.1", + "egg-cors": "^2.1.0", "egg-mongoose": "^3.1.0", "egg-oss": "^1.1.0", "egg-redis": "^2.0.0", From 4c745e57d151a28fd2c5e12ac900378169f1238b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 4 Sep 2018 15:59:35 +0800 Subject: [PATCH 136/208] update: add ali ip lookup service --- app/controller/comment.js | 2 +- app/controller/stat.js | 5 ++++ app/extend/context.js | 12 ++++++--- app/model/setting.js | 6 +++++ app/service/aliApi.js | 53 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 app/controller/stat.js create mode 100644 app/service/aliApi.js diff --git a/app/controller/comment.js b/app/controller/comment.js index 5e0025f..11cdd21 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -178,7 +178,7 @@ module.exports = class CommentController extends Controller { if (!spamValid) { return ctx.fail('该用户的垃圾评论数量已达到最大限制,已被禁言') } - const { ip, location } = ctx.getLocation() + const { ip, location } = await ctx.getLocation() const meta = body.meta = { location, ip, diff --git a/app/controller/stat.js b/app/controller/stat.js new file mode 100644 index 0000000..6089279 --- /dev/null +++ b/app/controller/stat.js @@ -0,0 +1,5 @@ +const { Controller } = require('egg') + +module.exports = class StatController extends Controller { + +} diff --git a/app/extend/context.js b/app/extend/context.js index 078f89e..e578c6c 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -54,7 +54,7 @@ module.exports = { this.throw(422, '发布人不存在') } }, - getLocation () { + async getLocation () { const req = this.req const ip = (req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || @@ -63,9 +63,13 @@ module.exports = { req.connection.socket.remoteAddress || req.ip || req.ips[0] || '').replace('::ffff:', '') - return { - ip, - location: geoip.lookup(ip) || {} + let location = {} + const { success, data } = await this.service.aliApi.lookupIp(ip) + if (success) { + location = data + } else { + location = geoip.lookup(ip) || {} } + return { ip, location } } } diff --git a/app/model/setting.js b/app/model/setting.js index 3ddf3d9..d1723f0 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -39,6 +39,12 @@ module.exports = app => { appid: { type: String, default: '' }, secret: { type: String, default: '' } }, + aliApiGateway: { + // 查询IP + ip: { + appCode: { type: String, default: '' } + } + }, // 163邮箱 mail: { user: { type: String, default: '' }, diff --git a/app/service/aliApi.js b/app/service/aliApi.js new file mode 100644 index 0000000..50bd939 --- /dev/null +++ b/app/service/aliApi.js @@ -0,0 +1,53 @@ +/** + * @desc 阿里Api市场服务 Services + */ + +const https = require('https') +const { Service } = require('egg') + +module.exports = class MailService extends Service { + lookupIp (ip) { + return new Promise(resolve => { + const req = https.request({ + hostname: 'dm-81.data.aliyun.com', + port: 443, + path: `/rest/160601/ip/getIpInfo.json?ip=${ip}`, + method: 'GET', + protocol: 'https:', + headers: { + Authorization: `APPCODE ${this.app.setting.keys.aliApiGateway.ip.appCode}` + } + }, res => { + let success = false + let data = null + if (res.statusCode === 200) { + success = true + } + res.on('data', d => { + data = JSON.parse(d) + }) + res.on('end', () => { + if (success && data && !data.code) { + this.app.logger.info('IP地址查询成功,ip:' + ip) + return resolve({ + success: true, + data: data.data + }) + } + resolve({ + success: false, + data + }) + }) + }) + req.on('error', err => { + this.app.logger.error(err) + resolve({ + success: false, + data: err + }) + }) + req.end() + }) + } +} From fbb2cb19ceb0ac2bff056169b149268eb43a5238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 4 Sep 2018 16:14:24 +0800 Subject: [PATCH 137/208] update: add links update schedule task --- app/schedule/links.js | 19 +++++++++++++++++++ app/service/setting.js | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 app/schedule/links.js diff --git a/app/schedule/links.js b/app/schedule/links.js new file mode 100644 index 0000000..8a06351 --- /dev/null +++ b/app/schedule/links.js @@ -0,0 +1,19 @@ +/** + * @desc 友链更新定时任务 + */ + +const { Subscription } = require('egg') + +module.exports = class Links extends Subscription { + static get schedule () { + return { + // 每天0点更新一次 + cron: '0 0 * * *', + type: 'all' + } + } + + async task () { + await this.service.setting.updateLinks() + } +} diff --git a/app/service/setting.js b/app/service/setting.js index 1893664..fd93277 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -80,6 +80,6 @@ module.exports = class SettingService extends ProxyService { setting = await this.getItem() } this.app.setting = setting || null - this.logger.info('配置挂载App成功') + this.logger.info('配置挂载成功') } } From b68593ea37c7f19bc6315f3fc85ca0fcfee1b314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 4 Sep 2018 22:48:21 +0800 Subject: [PATCH 138/208] update: add stat model --- .eslintrc | 3 +- app/controller/article.js | 25 +++++- app/controller/stat.js | 5 +- app/lib/plugin/egg-akismet/lib/akismet.js | 11 ++- app/model/stat.js | 35 +++++++++ app/router/backend.js | 3 + app/service/article.js | 26 +++++-- app/service/stat.js | 93 +++++++++++++++++++++++ config/config.default.js | 12 +++ package.json | 1 + 10 files changed, 195 insertions(+), 19 deletions(-) create mode 100644 app/model/stat.js create mode 100644 app/service/stat.js diff --git a/.eslintrc b/.eslintrc index e01f267..7572916 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,6 +7,7 @@ "strict": 0, "comma-dangle": 0, "array-bracket-spacing": 0, - "no-use-before-define": 0 + "no-use-before-define": 0, + "no-constant-condition": 0 } } diff --git a/app/controller/article.js b/app/controller/article.js index f168f96..41907ad 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -116,9 +116,18 @@ module.exports = class ArticleController extends Controller { } } } - console.log(query) - const data = await this.service.article.getLimitListByQuery(ctx.processPayload(query), options) + const statService = this.service.stat + // 生成搜索统计 + if (query.category) { + statService.record('CATEGORY_SEARCH', { category: query.category }, 'count') + } + if (query.tag) { + statService.record('TAG_SEARCH', { tag: query.tag }, 'count') + } + if (keyword) { + statService.record('KEYWORD_SEARCH', { keyword }, 'count') + } data ? ctx.success(data, '文章列表获取成功') : ctx.fail('文章列表获取失败') @@ -128,6 +137,10 @@ module.exports = class ArticleController extends Controller { const { ctx } = this const params = ctx.validateParamsObjectId() const data = await this.service.article.getItemById(params.id) + if (!this.ctx.session._isAuthed) { + // 生成 pv 统计项 + this.service.stat.record('ARTICLE_VIEW', { article: params.id }, 'count') + } data ? ctx.success(data, '文章详情获取成功') : ctx.fail('文章详情获取失败') @@ -194,8 +207,12 @@ module.exports = class ArticleController extends Controller { } }) if (data) { - // 生成like通告 - this.service.notification.recordLike('article', data, ctx.request.body.user, true) + if (!this.ctx.session._isAuthed) { + // 生成like通告 + this.service.notification.recordLike('article', data, ctx.request.body.user, true) + // 生成 like 统计项 + this.service.stat.record('ARTICLE_LIKE', { article: params.id }, 'count') + } ctx.success('文章点赞成功') } else { ctx.fail('文章点赞失败') diff --git a/app/controller/stat.js b/app/controller/stat.js index 6089279..3e6177a 100644 --- a/app/controller/stat.js +++ b/app/controller/stat.js @@ -1,5 +1,8 @@ const { Controller } = require('egg') module.exports = class StatController extends Controller { - + async count () { + const data = await this.service.stat.count() + this.ctx.success(data) + } } diff --git a/app/lib/plugin/egg-akismet/lib/akismet.js b/app/lib/plugin/egg-akismet/lib/akismet.js index 5ee2ff5..372cfec 100644 --- a/app/lib/plugin/egg-akismet/lib/akismet.js +++ b/app/lib/plugin/egg-akismet/lib/akismet.js @@ -2,18 +2,17 @@ const akismet = require('akismet-api') module.exports = app => { app.addSingleton('akismet', createClient) - app.beforeStart(async () => { - try { - const valid = await app.akismet.verifyKey() + app.beforeStart(() => { + app.akismet.verifyKey().then(valid => { if (valid) { app.coreLogger.info('[egg-akismet] 服务启动成功') app._akismetValid = true } else { app.coreLogger.error('[egg-akismet] 服务启动失败:无效的Apikey') } - } catch (error) { - app.coreLogger.error('[egg-akismet] ' + error.message) - } + }).catch(err => { + app.coreLogger.error('[egg-akismet] ' + err.message) + }) }) } diff --git a/app/model/stat.js b/app/model/stat.js new file mode 100644 index 0000000..cb36c1c --- /dev/null +++ b/app/model/stat.js @@ -0,0 +1,35 @@ +/** + * @desc 统计模型 + */ + +module.exports = app => { + const { mongoose, config } = app + const { Schema } = mongoose + const statValidateConfig = config.modelEnum.stat + + const StatSchema = new Schema({ + // 类型 + type: { + type: Number, + required: true, + validate: val => Object.values(statValidateConfig.type.optional).includes(val) + }, + // 统计目标 + target: { + keyword: { type: String, required: false }, + article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article', required: false }, + category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category', required: false }, + tag: { type: mongoose.Schema.Types.ObjectId, ref: 'Tag', required: false } + }, + // 统计项 + stat: { + count: { type: Number, required: false, default: 0 } + }, + // 创建日期 + createdAt: { type: Date, default: Date.now }, + // 更新日期 + updatedAt: { type: Date, default: Date.now } + }) + + return mongoose.model('Stat', app.processSchema(StatSchema)) +} diff --git a/app/router/backend.js b/app/router/backend.js index bb16935..8cc984b 100644 --- a/app/router/backend.js +++ b/app/router/backend.js @@ -58,4 +58,7 @@ module.exports = app => { backendRouter.patch('/notifications/view', auth, controller.notification.viewAll) backendRouter.patch('/notifications/:id/view', auth, controller.notification.view) backendRouter.delete('/notifications/:id', auth, controller.notification.delete) + + // Stat + backendRouter.get('/stat/count', auth, controller.stat.count) } diff --git a/app/service/article.js b/app/service/article.js index 4b31394..e58447d 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -10,16 +10,28 @@ module.exports = class ArticleService extends ProxyService { } async getItemById (id, select, opt = {}, single = false) { - let api = this.getItem.bind(this) - const query = { _id: id } + let data = null + const populate = [ + { + path: 'category', + select: 'name description extends' + }, + { + path: 'tag', + select: 'name description extends' + } + ] if (!this.ctx.session._isAuthed) { - api = this.updateItem.bind(this) // 前台博客访问文章的时候pv+1 - query.state = this.config.modelEnum.article.state.optional.PUBLISH - select += ' -content' - opt.$inc = { 'meta.pvs': 1 } + data = await this.updateItem({ + _id: id, + state: this.config.modelEnum.article.state.optional.PUBLISH + }, { + $inc: { 'meta.pvs': 1 } + }, opt, populate) + } else { + data = await this.getItem({ _id: id }, '-content', opt, populate) } - const data = await api(query, select, opt) if (data && !single) { // 获取相关文章和上下篇文章 const [related, adjacent] = await Promise.all([ diff --git a/app/service/stat.js b/app/service/stat.js new file mode 100644 index 0000000..a0fdada --- /dev/null +++ b/app/service/stat.js @@ -0,0 +1,93 @@ +/** + * @desc 各类统计 Service + */ + +const moment = require('moment') +const ProxyService = require('./proxy') + +module.exports = class StatService extends ProxyService { + get model () { + return this.app.model.Stat + } + + async record (typeKey, target = {}, statKey) { + const statConfig = this.app.config.modelEnum.stat.type.optional + const type = statConfig[typeKey] + const stat = { [statKey]: 1 } + const payload = { type, target, stat } + const data = await this.create(payload) + if (data) { + this.logger.info(`统计项生成成功,[id: ${data._id}] [type:${typeKey}] [stat:${statKey}]`) + } + } + + async count () { + return this.countFromToday(10, 'ARTICLE_LIKE') + } + + countToday (type) { + return this.countFromToday(0, type) + } + + countWeek (type) { + return this.countFromToday(7, type) + } + + countFromToday (subtract, type) { + const today = new Date() + const before = moment().subtract(subtract, 'days') + return this.countRange(before, today, type) + } + + async countRange (start, end, type) { + const statConfig = this.app.config.modelEnum.stat.type.optional + const sm = moment(start) + const em = moment(end) + const $sort = { + createdAt: 1 + } + const $match = { + type: statConfig[type], + createdAt: { + $gte: new Date(sm.format('YYYY-MM-DD 00:00:00')), + $lte: new Date(em.format('YYYY-MM-DD 23:59:59')) + } + } + const $project = { + _id: 0, + 'stat.count': 1, + createdAt: 1, + date: { + $dateToString: { + format: '%Y-%m-%d', + date: '$createdAt' + } + } + } + const $group = { + _id: '$date', + count: { + $sum: '$stat.count' + }, + } + const data = await this.aggregate([ + { $sort }, + { $match }, + { $project }, + { $group } + ]) + const diff = Math.ceil(em.diff(sm) / 60 / 60 / 24 / 1000) + return new Array(diff || 1).fill().map((item, index) => { + const date = moment().subtract(index, 'days').format('YYYY-MM-DD') + let count = 0 + const hit = data.find(d => d._id === date) + if (hit) { + count = hit.count + } + return { + date, + count + } + }) + } +} diff --git a/config/config.default.js b/config/config.default.js index 98bff1c..2cdb517 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -199,6 +199,18 @@ module.exports = appInfo => { USER_USER_UPDATE: 'user_update' // 用户更新 } } + }, + stat: { + type: { + optional: { + // 遵循 target_action 模式 + KEYWORD_SEARCH: 0, // 文章关键词搜索 + CATEGORY_SEARCH: 1, // 文章分类搜索 + TAG_SEARCH: 2, // 文章标签搜索 + ARTICLE_VIEW: 3, // 文章访问 + ARTICLE_LIKE: 4 // 文章点赞 + } + } } } diff --git a/package.json b/package.json index a015cb1..0456dbc 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "koa-is-json": "^1.0.0", "lodash": "^4.17.10", "marked": "^0.5.0", + "moment": "^2.22.2", "mongoose": "5.2.8", "mongoose-paginate-v2": "^1.0.12", "node-homedir": "^1.1.1", From af5f2ec31dd7fce2c0bc75c7ff47bc421a0237b9 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 5 Sep 2018 02:29:08 +0800 Subject: [PATCH 139/208] fix: links update --- app/controller/auth.js | 3 ++- app/model/setting.js | 1 - app/model/user.js | 1 + app/service/category.js | 6 ++++-- app/service/setting.js | 2 +- app/service/tag.js | 14 ++++++++------ 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/controller/auth.js b/app/controller/auth.js index 1d5e43a..119e709 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -21,7 +21,8 @@ module.exports = class AuthController extends Controller { avatar: { type: 'string', required: false }, slogan: { type: 'string', required: false }, company: { type: 'string', required: false }, - location: { type: 'string', required: false } + location: { type: 'string', required: false }, + tags: { type: 'array', required: false } }, password: { password: { type: 'string', required: true, min: 6 }, diff --git a/app/model/setting.js b/app/model/setting.js index d1723f0..5a5f8cd 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -13,7 +13,6 @@ module.exports = app => { description: { type: String, default: '' }, hobby: { type: String, default: '' }, skill: { type: String, default: '' }, - music: { type: String, default: '' }, location: { type: String, default: '' }, company: { type: String, default: '' }, links: [{ diff --git a/app/model/user.js b/app/model/user.js index 5e31cd9..219dcd4 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -20,6 +20,7 @@ module.exports = app => { default: userValidateConfig.role.default, validate: val => Object.values(userValidateConfig.role.optional).includes(val) }, + tags: [{ type: String }], // role = 0的时候才有该项 password: { type: String }, // 是否被禁言 diff --git a/app/service/category.js b/app/service/category.js index 1c49a12..949d1c0 100644 --- a/app/service/category.js +++ b/app/service/category.js @@ -13,16 +13,18 @@ module.exports = class CategoryService extends ProxyService { opt = this.app.merge({ sort: '-createdAt' }, opt) - const categories = await this.model.find(query, select, opt).exec() + let categories = await this.model.find(query, select, opt).exec() if (categories.length) { const PUBLISH = this.app.config.modelEnum.article.state.optional.PUBLISH - await Promise.all( + categories = await Promise.all( categories.map(async item => { + item = item.toObject() const articles = await this.service.article.getList({ category: item._id, state: PUBLISH }) item.count = articles.length + return item }) ) } diff --git a/app/service/setting.js b/app/service/setting.js index fd93277..fac4b96 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -42,8 +42,8 @@ module.exports = class SettingService extends ProxyService { link.avatar = this.app.proxyUrl(userInfo.avatar_url) link.slogan = userInfo.bio link.site = link.site || userInfo.blog || userInfo.url - return link } + return link } return null }) diff --git a/app/service/tag.js b/app/service/tag.js index 2932d8b..9dc826d 100644 --- a/app/service/tag.js +++ b/app/service/tag.js @@ -13,19 +13,21 @@ module.exports = class TagService extends ProxyService { opt = this.app.merge({ sort: '-createdAt' }, opt) - const categories = await this.model.find(query, select, opt).exec() - if (categories.length) { + let tag = await this.model.find(query, select, opt).exec() + if (tag.length) { const PUBLISH = this.app.config.modelEnum.article.state.optional.PUBLISH - await Promise.all( - categories.map(async item => { + tag = await Promise.all( + tag.map(async item => { + item = item.toObject() const articles = await this.service.article.getList({ - category: item._id, + tag: item._id, state: PUBLISH }) item.count = articles.length + return item }) ) } - return categories + return tag } } From 814b58113476eab02e95f46a550995dabb0c03b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 5 Sep 2018 15:45:10 +0800 Subject: [PATCH 140/208] update: add stat service(eg trend and count) --- app/controller/stat.js | 47 +++++++++++++- app/router/backend.js | 1 + app/service/stat.js | 140 ++++++++++++++++++++++++++++++++++------- 3 files changed, 164 insertions(+), 24 deletions(-) diff --git a/app/controller/stat.js b/app/controller/stat.js index 3e6177a..3c62ec6 100644 --- a/app/controller/stat.js +++ b/app/controller/stat.js @@ -1,8 +1,51 @@ const { Controller } = require('egg') module.exports = class StatController extends Controller { + get rules () { + return { + trend: { + startDate: { type: 'dateTime', required: true }, + endDate: { type: 'dateTime', required: true }, + dimension: { + type: 'enum', + values: this.service.stat.dimensionsValidate, + required: true + }, + target: { type: 'string', required: true } + } + } + } + async count () { - const data = await this.service.stat.count() - this.ctx.success(data) + // 文章浏览量 文章点赞数 文章评论量 站内留言量 + const [pv, like, comment, message] = await Promise.all( + ['pv', 'like', 'comment', 'message'].map(type => { + return this.service.stat.getCount(type) + }) + ) + this.ctx.success({ + pv, + like, + comment, + message + }, '获取数量统计成功') + } + + async trend () { + const { ctx } = this + ctx.validate(this.rules.trend, ctx.query) + const { startDate, endDate, dimension, target } = ctx.query + const trend = await this.service.stat.trendRange( + startDate, + endDate, + dimension, + target + ) + this.ctx.success({ + dimension, + startDate, + endDate, + trend + }, '获取趋势统计成功') } } diff --git a/app/router/backend.js b/app/router/backend.js index 8cc984b..7c7a15e 100644 --- a/app/router/backend.js +++ b/app/router/backend.js @@ -61,4 +61,5 @@ module.exports = app => { // Stat backendRouter.get('/stat/count', auth, controller.stat.count) + backendRouter.get('/stat/trend', auth, controller.stat.trend) } diff --git a/app/service/stat.js b/app/service/stat.js index a0fdada..26ccb7d 100644 --- a/app/service/stat.js +++ b/app/service/stat.js @@ -10,9 +10,36 @@ module.exports = class StatService extends ProxyService { return this.app.model.Stat } + get statConfig () { + return this.app.config.modelEnum.stat.type.optional + } + + get dimensions () { + return { + day: { + type: 'day', + format: '%Y-%m-%d', + mFormat: 'YYYY-MM-DD' + }, + month: { + type: 'month', + format: '%Y-%m', + mFormat: 'YYYY-MM' + }, + year: { + type: 'year', + format: '%Y', + mFormat: 'YYYY' + } + } + } + + get dimensionsValidate () { + return Object.values(this.dimensions).map(item => item.type) + } + async record (typeKey, target = {}, statKey) { - const statConfig = this.app.config.modelEnum.stat.type.optional - const type = statConfig[typeKey] + const type = this.statConfig[typeKey] const stat = { [statKey]: 1 } const payload = { type, target, stat } const data = await this.create(payload) @@ -21,45 +48,89 @@ module.exports = class StatService extends ProxyService { } } - async count () { - return this.countFromToday(10, 'ARTICLE_LIKE') + async getCount (type) { + const [today, total] = await Promise.all([ + this.countToday(type), + this.countTotal(type) + ]) + return { today, total } } countToday (type) { return this.countFromToday(0, type) } - countWeek (type) { - return this.countFromToday(7, type) + countTotal (type) { + return this.countFromToday(null, type) } countFromToday (subtract, type) { const today = new Date() - const before = moment().subtract(subtract, 'days') + const before = (subtract !== null) ? moment().subtract(subtract, 'days') : subtract return this.countRange(before, today, type) } async countRange (start, end, type) { - const statConfig = this.app.config.modelEnum.stat.type.optional - const sm = moment(start) - const em = moment(end) + let sm = start && moment(start) + let em = end && moment(end) + let service = null + const filter = { + createdAt: {} + } + if (sm) { + const format = sm.format('YYYY-MM-DD 00:00:00') + sm = moment(format) + filter.createdAt.$gte = new Date(format) + } + if (em) { + const format = em.format('YYYY-MM-DD 23:59:59') + em = moment(format) + filter.createdAt.$lte = new Date(em.format('YYYY-MM-DD 23:59:59')) + } + if (type === 'pv') { + service = this + filter.type = this.statConfig.ARTICLE_VIEW + } else if (type === 'like') { + service = this + filter.type = this.statConfig.ARTICLE_LIKE + } else if (type === 'comment') { + // 文章评论量 + service = this.service.comment + filter.type = this.config.modelEnum.comment.type.optional.COMMENT + } else if (type === 'message') { + // 站内留言量 + service = this.service.comment + filter.type = this.config.modelEnum.comment.type.optional.MESSAGE + } + return service && service.count(filter) || null + } + + async trendRange (start, end, dimension, type) { + let sm = moment(start) + let em = moment(end) + let service = null const $sort = { createdAt: 1 } const $match = { - type: statConfig[type], - createdAt: { - $gte: new Date(sm.format('YYYY-MM-DD 00:00:00')), - $lte: new Date(em.format('YYYY-MM-DD 23:59:59')) - } + createdAt: {} + } + if (sm) { + const format = sm.format('YYYY-MM-DD 00:00:00') + sm = moment(format) + $match.createdAt.$gte = new Date(format) + } + if (em) { + const format = em.format('YYYY-MM-DD 23:59:59') + em = moment(format) + $match.createdAt.$lte = new Date(em.format('YYYY-MM-DD 23:59:59')) } const $project = { _id: 0, - 'stat.count': 1, createdAt: 1, date: { $dateToString: { - format: '%Y-%m-%d', + format: this.dimensions[dimension].format, date: '$createdAt' } } @@ -67,18 +138,43 @@ module.exports = class StatService extends ProxyService { const $group = { _id: '$date', count: { - $sum: '$stat.count' - }, + $sum: 1 + } + } + if (type === 'pv') { + service = this + $match.type = this.statConfig.ARTICLE_VIEW + } else if (type === 'like') { + service = this + $match.type = this.statConfig.ARTICLE_LIKE + } else if (type === 'comment') { + // 文章评论量 + service = this.service.comment + $match.type = this.config.modelEnum.comment.type.optional.COMMENT + } else if (type === 'message') { + // 站内留言量 + service = this.service.comment + $match.type = this.config.modelEnum.comment.type.optional.MESSAGE } - const data = await this.aggregate([ + if (!service) return [] + const data = await service.aggregate([ { $sort }, { $match }, { $project }, { $group } ]) - const diff = Math.ceil(em.diff(sm) / 60 / 60 / 24 / 1000) + // day维度 + let radix = 1000 * 60 * 60 * 24 + if (dimension === this.dimensions.month.type) { + // month 维度 + radix *= 30 + } else if (dimension === this.dimensions.year.type) { + // year 维度 + radix *= 365 + } + const diff = Math.ceil(em.diff(sm) / radix) return new Array(diff || 1).fill().map((item, index) => { - const date = moment().subtract(index, 'days').format('YYYY-MM-DD') + const date = moment(em).subtract(index, dimension + 's').format(this.dimensions[dimension].mFormat) let count = 0 const hit = data.find(d => d._id === date) if (hit) { From ae9091d3eff0bf1dfa8ad81152cdb4318a30a590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 5 Sep 2018 19:06:45 +0800 Subject: [PATCH 141/208] update: setting add github field --- app/controller/setting.js | 12 +++++------- app/model/setting.js | 16 +++++++++++----- app/model/user.js | 18 +++++++++--------- app/schedule/links.js | 2 +- app/schedule/personal.js | 19 +++++++++++++++++++ app/service/article.js | 6 +++--- app/service/auth.js | 9 +-------- app/service/setting.js | 24 ++++++++++++++++++++++++ app/service/stat.js | 4 ++-- app/service/user.js | 4 +--- 10 files changed, 76 insertions(+), 38 deletions(-) create mode 100644 app/schedule/personal.js diff --git a/app/controller/setting.js b/app/controller/setting.js index a3562a3..3e5e05f 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -10,13 +10,9 @@ module.exports = class SettingController extends Controller { index: { filter: { type: 'string', required: false } }, - create: { - site: { type: 'object', required: false }, - keys: { type: 'object', required: false }, - limit: { type: 'object', required: false } - }, update: { site: { type: 'object', required: false }, + personal: { type: 'object', required: false }, keys: { type: 'object', required: false }, limit: { type: 'object', required: false } } @@ -38,7 +34,7 @@ module.exports = class SettingController extends Controller { async update () { const { ctx } = this - let body = ctx.validateBody(this.rules.create) + let body = ctx.validateBody(this.rules.update) const exist = await this.service.setting.getItem() if (!exist) { return ctx.fail('配置未找到') @@ -46,7 +42,9 @@ module.exports = class SettingController extends Controller { body = this.app.merge({}, exist, body) await this.service.setting.updateItemById(exist._id, body) // 抓取友链 - const data = await this.service.setting.updateLinks() + await this.service.setting.updateLinks() + // 更新github信息 + const data = await this.service.setting.updateGithubInfo() data ? ctx.success(data, '配置更新成功') : ctx.fail('配置更新失败') diff --git a/app/model/setting.js b/app/model/setting.js index 5a5f8cd..b369470 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -10,11 +10,6 @@ module.exports = app => { // 站点设置 site: { welcome: { type: String, default: '' }, - description: { type: String, default: '' }, - hobby: { type: String, default: '' }, - skill: { type: String, default: '' }, - location: { type: String, default: '' }, - company: { type: String, default: '' }, links: [{ name: { type: String, required: true }, github: { type: String, default: '' }, @@ -24,6 +19,17 @@ module.exports = app => { }], musicId: { type: String, default: '' } }, + // 个人信息 + personal: { + slogan: { type: String }, + description: { type: String, default: '' }, + tag: [{ type: String }], + hobby: [{ type: String }], + skill: [{ type: String }], + location: { type: String, default: '' }, + company: { type: String, default: '' }, + github: { type: Object, default: {} } + }, // 第三方插件的参数 keys: { // 阿里云oss diff --git a/app/model/user.js b/app/model/user.js index 219dcd4..8df71fc 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -12,28 +12,28 @@ module.exports = app => { email: { type: String, required: true, validate: app.utils.validate.isEmail }, avatar: { type: String, required: true }, site: { type: String, validate: app.utils.validate.isUrl }, - slogan: { type: String }, - description: { type: String, default: '' }, + // slogan: { type: String }, + // description: { type: String, default: '' }, // 角色 0 管理员 | 1 普通用户 role: { type: Number, default: userValidateConfig.role.default, validate: val => Object.values(userValidateConfig.role.optional).includes(val) }, - tags: [{ type: String }], + // tags: [{ type: String }], // role = 0的时候才有该项 password: { type: String }, // 是否被禁言 mute: { type: Boolean, default: false }, - company: { type: String, default: '' }, - location: { type: String, default: '' }, + // company: { type: String, default: '' }, + // location: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, // github信息,不能手动更改 - github: { - id: { type: String, default: '' }, - login: { type: String, default: '' } - } + // github: { + // id: { type: String, default: '' }, + // login: { type: String, default: '' } + // } }) return mongoose.model('User', app.processSchema(UserSchema)) diff --git a/app/schedule/links.js b/app/schedule/links.js index 8a06351..3b74734 100644 --- a/app/schedule/links.js +++ b/app/schedule/links.js @@ -8,7 +8,7 @@ module.exports = class Links extends Subscription { static get schedule () { return { // 每天0点更新一次 - cron: '0 0 * * *', + cron: '0 0 * * * *', type: 'all' } } diff --git a/app/schedule/personal.js b/app/schedule/personal.js new file mode 100644 index 0000000..6215956 --- /dev/null +++ b/app/schedule/personal.js @@ -0,0 +1,19 @@ +/** + * @desc 个人信息更新定时任务 + */ + +const { Subscription } = require('egg') + +module.exports = class Links extends Subscription { + static get schedule () { + return { + // 每小时更新一次 + cron: '0 0 */1 * * *', + type: 'all' + } + } + + async task () { + await this.service.setting.updateGithubInfo() + } +} diff --git a/app/service/article.js b/app/service/article.js index e58447d..868496f 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -78,14 +78,14 @@ module.exports = class ArticleService extends ProxyService { } } ]) - let count = 0 + let total = 0 if (data && data.length) { data = [...new Set(data.map(item => item._id.year))].map(year => { const months = [] data.forEach(item => { const { _id, articles } = item if (year === _id.year) { - count += articles.length + total += articles.length months.push({ month: _id.month, monthStr: this.app.utils.share.getMonthFromNum(_id.month), @@ -100,7 +100,7 @@ module.exports = class ArticleService extends ProxyService { }) } return { - count, + total, list: data || [] } } diff --git a/app/service/auth.js b/app/service/auth.js index c13acc9..80e481f 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -37,15 +37,8 @@ module.exports = class AuthService extends Service { name: userInfo.name, email: userInfo.email || this.config.author.email, password: this.app.utils.encode.bhash(defaultAdmin.password), - slogan: userInfo.bio, site: userInfo.blog || userInfo.url, - avatar: this.app.proxyUrl(userInfo.avatar_url), - company: userInfo.company, - location: userInfo.location, - github: { - id: userInfo.id, - login: userInfo.login - } + avatar: this.app.proxyUrl(userInfo.avatar_url) }) } } diff --git a/app/service/setting.js b/app/service/setting.js index fac4b96..77be0c4 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -18,6 +18,7 @@ module.exports = class SettingService extends ProxyService { if (exist) { return exist } + // TIP: 这里不能用create,create如果不传model,是不会创建的 const data = await this.newAndSave() if (data) { this.logger.info('Setting初始化成功') @@ -60,6 +61,7 @@ module.exports = class SettingService extends ProxyService { let setting = await this.getItem() if (!setting) return null const update = await this.generateLinks(setting.site.links) + if (!update.length) return setting = await this.updateItemById(setting._id, { $set: { 'site.links': update @@ -71,6 +73,28 @@ module.exports = class SettingService extends ProxyService { return setting } + /** + * @desc 更新personal的github信息 + * @return {Setting} 更新后的setting + */ + async updateGithubInfo () { + let setting = await this.getItem() + if (!setting) return null + const github = setting.personal.github + if (!github.login) return + const user = await this.service.github.getUserInfo(github.login) + if (!user) return + setting = await this.updateItemById(setting._id, { + $set: { + 'personal.github': user + } + }) + // 个人github信息更新成功 + this.logger.info('个人github信息更新成功') + this.mountToApp(setting) + return setting + } + /** * @desc 把配置挂载到app上 * @param {Setting} setting 配置 diff --git a/app/service/stat.js b/app/service/stat.js index 26ccb7d..234ce09 100644 --- a/app/service/stat.js +++ b/app/service/stat.js @@ -110,7 +110,7 @@ module.exports = class StatService extends ProxyService { let em = moment(end) let service = null const $sort = { - createdAt: 1 + createdAt: -1 } const $match = { createdAt: {} @@ -174,7 +174,7 @@ module.exports = class StatService extends ProxyService { } const diff = Math.ceil(em.diff(sm) / radix) return new Array(diff || 1).fill().map((item, index) => { - const date = moment(em).subtract(index, dimension + 's').format(this.dimensions[dimension].mFormat) + const date = moment(sm).add(index, dimension + 's').format(this.dimensions[dimension].mFormat) let count = 0 const hit = data.find(d => d._id === date) if (hit) { diff --git a/app/service/user.js b/app/service/user.js index 93084f7..2cbf5b4 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -42,10 +42,8 @@ module.exports = class UserService extends ProxyService { const update = {} author.name && (update.name = author.name) author.site && (update.site = author.site) + author.email && (update.email = author.email) update.avatar = this.app.utils.gravatar(author.email) - if (author.email) { - update.email = author.email - } const id = author.id || author._id if (id) { // 更新 From 13f83b42f1bcb82fe1da2b1ea62571174f4e1ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 5 Sep 2018 20:08:30 +0800 Subject: [PATCH 142/208] fix: comment reponse meta,ip field --- app/controller/comment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controller/comment.js b/app/controller/comment.js index 11cdd21..f7d1b17 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -109,7 +109,7 @@ module.exports = class CommentController extends Controller { query.state = 1 query.spam = false // 评论列表不需要content和state - options.select = '-content -state -updatedAt -spam -type' + options.select = '-content -state -updatedAt -spam -type -meta.ip' } else { // 排序 if (sortBy && order) { From cb4c22a59289c0685b0f14036bb9b735db8357fa Mon Sep 17 00:00:00 2001 From: Jooger Date: Thu, 6 Sep 2018 02:21:09 +0800 Subject: [PATCH 143/208] fix: fix stat trend service GMT date to ISODate --- app/controller/setting.js | 12 +++++++----- app/controller/stat.js | 1 + app/extend/application.js | 8 +++++++- app/model/article.js | 4 ---- app/model/category.js | 4 ---- app/model/setting.js | 2 +- app/model/stat.js | 6 +----- app/model/tag.js | 4 ---- app/model/user.js | 14 +------------- app/service/stat.js | 6 ++++-- 10 files changed, 22 insertions(+), 39 deletions(-) diff --git a/app/controller/setting.js b/app/controller/setting.js index 3e5e05f..30b41d8 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -34,15 +34,17 @@ module.exports = class SettingController extends Controller { async update () { const { ctx } = this - let body = ctx.validateBody(this.rules.update) + const body = ctx.validateBody(this.rules.update) const exist = await this.service.setting.getItem() if (!exist) { return ctx.fail('配置未找到') } - body = this.app.merge({}, exist, body) - await this.service.setting.updateItemById(exist._id, body) - // 抓取友链 - await this.service.setting.updateLinks() + const update = this.app.merge({}, exist, body) + await this.service.setting.updateItemById(exist._id, update) + if (body.site && body.site.links) { + // 抓取友链 + await this.service.setting.updateLinks() + } // 更新github信息 const data = await this.service.setting.updateGithubInfo() data diff --git a/app/controller/stat.js b/app/controller/stat.js index 3c62ec6..dcbaf33 100644 --- a/app/controller/stat.js +++ b/app/controller/stat.js @@ -42,6 +42,7 @@ module.exports = class StatController extends Controller { target ) this.ctx.success({ + target, dimension, startDate, endDate, diff --git a/app/extend/application.js b/app/extend/application.js index c278b50..04fbbb4 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -12,7 +12,13 @@ module.exports = { schema.set('versionKey', false) schema.set('toObject', { getters: true, virtuals: false }) schema.set('toJSON', { getters: true, virtuals: false }) - if (options.paginate) { + schema.add({ + // 创建日期 + createdAt: { type: Date, default: Date.now }, + // 更新日期 + updatedAt: { type: Date, default: Date.now } + }) + if (options && options.paginate) { schema.plugin(mongoosePaginate) } schema.pre('findOneAndUpdate', function (next) { diff --git a/app/model/article.js b/app/model/article.js index 0652a3b..36dc30b 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -36,10 +36,6 @@ module.exports = app => { default: articleValidateConfig.state.default, validate: val => Object.values(articleValidateConfig.state.optional).includes(val) }, - // 创建日期 - createdAt: { type: Date, default: Date.now }, - // 更新日期 - updatedAt: { type: Date, default: Date.now }, // 发布日期 publishedAt: { type: Date, default: Date.now }, // 文章元数据 (浏览量, 喜欢数, 评论数) diff --git a/app/model/category.js b/app/model/category.js index 570d631..bf15a7e 100644 --- a/app/model/category.js +++ b/app/model/category.js @@ -11,10 +11,6 @@ module.exports = app => { name: { type: String, required: true }, // 描述 description: { type: String, default: '' }, - // 创建日期 - createdAt: { type: Date, default: Date.now }, - // 更新日期 - updatedAt: { type: Date, default: Date.now }, // 扩展属性 extends: [{ key: { type: String, validate: /\S+/ }, diff --git a/app/model/setting.js b/app/model/setting.js index b369470..5bab80c 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -21,7 +21,7 @@ module.exports = app => { }, // 个人信息 personal: { - slogan: { type: String }, + slogan: { type: String, default: '' }, description: { type: String, default: '' }, tag: [{ type: String }], hobby: [{ type: String }], diff --git a/app/model/stat.js b/app/model/stat.js index cb36c1c..4d7e5b6 100644 --- a/app/model/stat.js +++ b/app/model/stat.js @@ -24,11 +24,7 @@ module.exports = app => { // 统计项 stat: { count: { type: Number, required: false, default: 0 } - }, - // 创建日期 - createdAt: { type: Date, default: Date.now }, - // 更新日期 - updatedAt: { type: Date, default: Date.now } + } }) return mongoose.model('Stat', app.processSchema(StatSchema)) diff --git a/app/model/tag.js b/app/model/tag.js index 519d5d6..638e50e 100644 --- a/app/model/tag.js +++ b/app/model/tag.js @@ -11,10 +11,6 @@ module.exports = app => { name: { type: String, required: true }, // 描述 description: { type: String, default: '' }, - // 创建日期 - createdAt: { type: Date, default: Date.now }, - // 更新日期 - updatedAt: { type: Date, default: Date.now }, // 扩展属性 extends: [{ key: { type: String, validate: /\S+/ }, diff --git a/app/model/user.js b/app/model/user.js index 8df71fc..ce08358 100644 --- a/app/model/user.js +++ b/app/model/user.js @@ -12,28 +12,16 @@ module.exports = app => { email: { type: String, required: true, validate: app.utils.validate.isEmail }, avatar: { type: String, required: true }, site: { type: String, validate: app.utils.validate.isUrl }, - // slogan: { type: String }, - // description: { type: String, default: '' }, // 角色 0 管理员 | 1 普通用户 role: { type: Number, default: userValidateConfig.role.default, validate: val => Object.values(userValidateConfig.role.optional).includes(val) }, - // tags: [{ type: String }], // role = 0的时候才有该项 password: { type: String }, // 是否被禁言 - mute: { type: Boolean, default: false }, - // company: { type: String, default: '' }, - // location: { type: String, default: '' }, - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, - // github信息,不能手动更改 - // github: { - // id: { type: String, default: '' }, - // login: { type: String, default: '' } - // } + mute: { type: Boolean, default: false } }) return mongoose.model('User', app.processSchema(UserSchema)) diff --git a/app/service/stat.js b/app/service/stat.js index 234ce09..8aaf0e9 100644 --- a/app/service/stat.js +++ b/app/service/stat.js @@ -123,7 +123,7 @@ module.exports = class StatService extends ProxyService { if (em) { const format = em.format('YYYY-MM-DD 23:59:59') em = moment(format) - $match.createdAt.$lte = new Date(em.format('YYYY-MM-DD 23:59:59')) + $match.createdAt.$lte = new Date(format) } const $project = { _id: 0, @@ -131,7 +131,9 @@ module.exports = class StatService extends ProxyService { date: { $dateToString: { format: this.dimensions[dimension].format, - date: '$createdAt' + date: '$createdAt', + // TIP: mongod是ISODate,是GMT-8h + timezone: '+08' } } } From 415ed64c1816964e190c53b33f34fb9bd8ac7237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Thu, 6 Sep 2018 20:29:22 +0800 Subject: [PATCH 144/208] update: change local cors allowOrigins --- config/config.local.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/config.local.js b/config/config.local.js index cca5e08..b66ad2e 100644 --- a/config/config.local.js +++ b/config/config.local.js @@ -10,8 +10,7 @@ module.exports = () => { config.security = { domainWhiteList: [ - 'http://localhost:8080', - 'http://127.0.0.1:8080' + '*' ], csrf: { ignore: () => true From b9d651e767d2ec27ed5ca9bf3d8d7124ce37a207 Mon Sep 17 00:00:00 2001 From: Jooger Date: Fri, 7 Sep 2018 01:27:47 +0800 Subject: [PATCH 145/208] update: add user create stat --- app/controller/article.js | 2 +- app/controller/auth.js | 2 +- app/controller/setting.js | 3 +++ app/controller/stat.js | 11 ++++++----- app/model/stat.js | 3 ++- app/service/comment.js | 4 ++-- app/service/stat.js | 16 +++++++++++++--- app/service/user.js | 1 + config/config.default.js | 3 ++- 9 files changed, 31 insertions(+), 14 deletions(-) diff --git a/app/controller/article.js b/app/controller/article.js index 41907ad..56f3cc9 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -51,7 +51,7 @@ module.exports = class ArticleController extends Controller { async list () { const { ctx } = this ctx.query.page = Number(ctx.query.page) - const tranArray = ['limit', 'state', 'source'] + const tranArray = ['limit', 'state', 'source', 'order'] tranArray.forEach(key => { if (ctx.query[key]) { ctx.query[key] = Number(ctx.query[key]) diff --git a/app/controller/auth.js b/app/controller/auth.js index 119e709..7ec8cdd 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -76,7 +76,7 @@ module.exports = class AuthController extends Controller { const exist = await this.service.user.getItemById(ctx.session._user._id) if (exist && exist.name !== body.name) { // 检测变更的name是否和其他用户冲突 - const conflict = await this.service.user.getItem({ name: ctx.session._user.name }) + const conflict = await this.service.user.getItem({ name: body.name }) if (conflict) { // 有冲突 return ctx.fail('用户名重复') diff --git a/app/controller/setting.js b/app/controller/setting.js index 30b41d8..d8e0634 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -26,6 +26,9 @@ module.exports = class SettingController extends Controller { if (ctx.query.filter) { select = ctx.query.filter } + if (!ctx.session._isAuthed) { + select = '-keys' + } const data = await this.service.setting.getItem({}, select) data ? ctx.success(data, '配置获取成功') diff --git a/app/controller/stat.js b/app/controller/stat.js index dcbaf33..981aace 100644 --- a/app/controller/stat.js +++ b/app/controller/stat.js @@ -15,19 +15,20 @@ module.exports = class StatController extends Controller { } } } - + async count () { // 文章浏览量 文章点赞数 文章评论量 站内留言量 - const [pv, like, comment, message] = await Promise.all( - ['pv', 'like', 'comment', 'message'].map(type => { + const [pv, up, comment, message, user] = await Promise.all( + ['pv', 'up', 'comment', 'message', 'user'].map(type => { return this.service.stat.getCount(type) }) ) this.ctx.success({ pv, - like, + up, comment, - message + message, + user }, '获取数量统计成功') } diff --git a/app/model/stat.js b/app/model/stat.js index 4d7e5b6..8755003 100644 --- a/app/model/stat.js +++ b/app/model/stat.js @@ -19,7 +19,8 @@ module.exports = app => { keyword: { type: String, required: false }, article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article', required: false }, category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category', required: false }, - tag: { type: mongoose.Schema.Types.ObjectId, ref: 'Tag', required: false } + tag: { type: mongoose.Schema.Types.ObjectId, ref: 'Tag', required: false }, + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: false } }, // 统计项 stat: { diff --git a/app/service/comment.js b/app/service/comment.js index ca037da..1dc8d1a 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -70,7 +70,7 @@ module.exports = class CommentService extends ProxyService { this.service.mail.sendToAdmin(typeTitle, { subject: adminTitle, text: `来自 ${comment.author.name} 的${adminType}:${comment.content}`, - html: `

来自 ${comment.author.name} 的${adminType} => 点击查看:${comment.renderedContent}

` + html: `

来自 ${comment.author.name} 的${adminType} => 点击查看:${comment.renderedContent}

` }) } @@ -82,7 +82,7 @@ module.exports = class CommentService extends ProxyService { to: forwardAuthor.email, subject: `你在 ${this.config.author.name} 的博客的评论有了新的回复`, text: `来自 ${comment.author.name} 的回复:${comment.content}`, - html: `

来自 ${comment.author.name} 的回复 => 点击查看:${comment.renderedContent}

` + html: `

来自 ${comment.author.name} 的回复 => 点击查看:${comment.renderedContent}

` }) } } diff --git a/app/service/stat.js b/app/service/stat.js index 8aaf0e9..fd9d951 100644 --- a/app/service/stat.js +++ b/app/service/stat.js @@ -85,12 +85,12 @@ module.exports = class StatService extends ProxyService { if (em) { const format = em.format('YYYY-MM-DD 23:59:59') em = moment(format) - filter.createdAt.$lte = new Date(em.format('YYYY-MM-DD 23:59:59')) + filter.createdAt.$lte = new Date(format) } if (type === 'pv') { service = this filter.type = this.statConfig.ARTICLE_VIEW - } else if (type === 'like') { + } else if (type === 'up') { service = this filter.type = this.statConfig.ARTICLE_LIKE } else if (type === 'comment') { @@ -101,7 +101,13 @@ module.exports = class StatService extends ProxyService { // 站内留言量 service = this.service.comment filter.type = this.config.modelEnum.comment.type.optional.MESSAGE + } else if (type === 'user') { + // 用户创建 + service = this + filter.type = this.statConfig.USER_CREATE } + console.log(filter); + return service && service.count(filter) || null } @@ -146,7 +152,7 @@ module.exports = class StatService extends ProxyService { if (type === 'pv') { service = this $match.type = this.statConfig.ARTICLE_VIEW - } else if (type === 'like') { + } else if (type === 'up') { service = this $match.type = this.statConfig.ARTICLE_LIKE } else if (type === 'comment') { @@ -157,6 +163,10 @@ module.exports = class StatService extends ProxyService { // 站内留言量 service = this.service.comment $match.type = this.config.modelEnum.comment.type.optional.MESSAGE + } else if (type === 'user') { + // 用户创建 + service = this + $match.type = this.statConfig.USER_CREATE } if (!service) return [] const data = await service.aggregate([ diff --git a/app/service/user.js b/app/service/user.js index 2cbf5b4..465d317 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -72,6 +72,7 @@ module.exports = class UserService extends ProxyService { })) if (user) { this.service.notification.recordUser(user, 'create') + this.service.stat.record('USER_CREATE', { user: user._id }, 'count') } } } diff --git a/config/config.default.js b/config/config.default.js index 2cdb517..320ed2c 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -208,7 +208,8 @@ module.exports = appInfo => { CATEGORY_SEARCH: 1, // 文章分类搜索 TAG_SEARCH: 2, // 文章标签搜索 ARTICLE_VIEW: 3, // 文章访问 - ARTICLE_LIKE: 4 // 文章点赞 + ARTICLE_LIKE: 4, // 文章点赞 + USER_CREATE: 5 // 用户创建 } } } From c267fba819048b0ad54f6ed0b49bdd6dc18ad33d Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 8 Sep 2018 02:22:08 +0800 Subject: [PATCH 146/208] update: update time validate type --- app/controller/article.js | 10 ++++++---- app/controller/comment.js | 4 ++-- app/controller/stat.js | 4 ++-- app/schedule/links.js | 2 +- app/schedule/personal.js | 2 +- app/service/article.js | 2 +- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/controller/article.js b/app/controller/article.js index 56f3cc9..b802d2f 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -15,8 +15,8 @@ module.exports = class ArticleController extends Controller { category: { type: 'objectId', required: false }, tag: { type: 'objectId', required: false }, keyword: { type: 'string', required: false }, - startDate: { type: 'dateTime', required: false }, - endDate: { type: 'dateTime', required: false }, + startDate: { type: 'string', required: false }, + endDate: { type: 'string', required: false }, // -1 desc | 1 asc order: { type: 'enum', values: [-1, 1], required: false }, sortBy: { type: 'enum', values: ['createdAt', 'updatedAt', 'publishedAt', 'meta.ups', 'meta.pvs', 'meta.comments'], required: false } @@ -31,7 +31,7 @@ module.exports = class ArticleController extends Controller { state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: true }, source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: true }, thumb: { type: 'url', required: false }, - createdAt: { type: 'dateTime', required: false } + createdAt: { type: 'string', required: false } }, update: { title: { type: 'string', required: false }, @@ -43,7 +43,7 @@ module.exports = class ArticleController extends Controller { state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: false }, source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: false }, thumb: { type: 'url', required: false }, - createdAt: { type: 'dateTime', required: false } + createdAt: { type: 'string', required: false } } } } @@ -175,6 +175,8 @@ module.exports = class ArticleController extends Controller { }, title: body.title }) + this.logger.info(exist); + if (exist) { return ctx.fail('文章名称重复') } diff --git a/app/controller/comment.js b/app/controller/comment.js index f7d1b17..0bc3a57 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -17,8 +17,8 @@ module.exports = class CommentController extends Controller { parent: { type: 'objectId', required: false }, keyword: { type: 'string', required: false }, // 时间区间查询仅后台可用,且依赖于createdAt - startDate: { type: 'dateTime', required: false }, - endDate: { type: 'dateTime', required: false }, + startDate: { type: 'string', required: false }, + endDate: { type: 'string', required: false }, // 排序仅后台能用,且order和sortBy需同时传入才起作用 // -1 desc | 1 asc order: { type: 'enum', values: [-1, 1], required: false }, diff --git a/app/controller/stat.js b/app/controller/stat.js index 981aace..009f367 100644 --- a/app/controller/stat.js +++ b/app/controller/stat.js @@ -4,8 +4,8 @@ module.exports = class StatController extends Controller { get rules () { return { trend: { - startDate: { type: 'dateTime', required: true }, - endDate: { type: 'dateTime', required: true }, + startDate: { type: 'string', required: true }, + endDate: { type: 'string', required: true }, dimension: { type: 'enum', values: this.service.stat.dimensionsValidate, diff --git a/app/schedule/links.js b/app/schedule/links.js index 3b74734..1431abf 100644 --- a/app/schedule/links.js +++ b/app/schedule/links.js @@ -13,7 +13,7 @@ module.exports = class Links extends Subscription { } } - async task () { + async subscribe () { await this.service.setting.updateLinks() } } diff --git a/app/schedule/personal.js b/app/schedule/personal.js index 6215956..612897c 100644 --- a/app/schedule/personal.js +++ b/app/schedule/personal.js @@ -13,7 +13,7 @@ module.exports = class Links extends Subscription { } } - async task () { + async subscribe () { await this.service.setting.updateGithubInfo() } } diff --git a/app/service/article.js b/app/service/article.js index 868496f..ed34639 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -30,7 +30,7 @@ module.exports = class ArticleService extends ProxyService { $inc: { 'meta.pvs': 1 } }, opt, populate) } else { - data = await this.getItem({ _id: id }, '-content', opt, populate) + data = await this.getItem({ _id: id }, opt, populate) } if (data && !single) { // 获取相关文章和上下篇文章 From 319c8e8bfebe11f0bb4a1f21d308fdfabab45136 Mon Sep 17 00:00:00 2001 From: Jooger Date: Sat, 8 Sep 2018 19:44:58 +0800 Subject: [PATCH 147/208] fix: fix bugs --- app.js | 6 ++---- app/controller/auth.js | 15 +++++++-------- app/controller/setting.js | 35 ++++++++++++++++++++++++++++++----- app/extend/application.js | 3 ++- app/model/notification.js | 10 +++++----- app/model/setting.js | 1 + app/service/setting.js | 6 +++++- config/config.prod.js | 5 ++++- package.json | 1 + 9 files changed, 57 insertions(+), 25 deletions(-) diff --git a/app.js b/app.js index c2987aa..b813f2f 100644 --- a/app.js +++ b/app.js @@ -8,12 +8,10 @@ module.exports = app => { mountSessionstoreToApp(app) app.beforeStart(async () => { const ctx = app.createAnonymousContext() - // 初始化配置(如果有必要) - await ctx.service.setting.seed() - // 配置挂载到App上 - await ctx.service.setting.mountToApp() // 初始化管理员(如果有必要) await ctx.service.auth.seed() + // 初始化配置(如果有必要) + await ctx.service.setting.seed() }) } diff --git a/app/controller/auth.js b/app/controller/auth.js index 7ec8cdd..f41f4f3 100644 --- a/app/controller/auth.js +++ b/app/controller/auth.js @@ -17,12 +17,7 @@ module.exports = class AuthController extends Controller { name: { type: 'string', required: false }, email: { type: 'email', required: false }, site: { type: 'url', required: false }, - description: { type: 'string', required: false }, - avatar: { type: 'string', required: false }, - slogan: { type: 'string', required: false }, - company: { type: 'string', required: false }, - location: { type: 'string', required: false }, - tags: { type: 'array', required: false } + avatar: { type: 'string', required: false } }, password: { password: { type: 'string', required: true, min: 6 }, @@ -73,7 +68,10 @@ module.exports = class AuthController extends Controller { async update () { const { ctx } = this const body = this.ctx.validateBody(this.rules.update) - const exist = await this.service.user.getItemById(ctx.session._user._id) + const exist = await this.service.user.getItemById( + ctx.session._user._id, + Object.keys(this.rules.update).join(' ') + ) if (exist && exist.name !== body.name) { // 检测变更的name是否和其他用户冲突 const conflict = await this.service.user.getItem({ name: body.name }) @@ -82,7 +80,8 @@ module.exports = class AuthController extends Controller { return ctx.fail('用户名重复') } } - const data = await this.service.user.updateItemById(ctx.session._user._id, body) + const update = this.app.merge({}, exist, body) + const data = await this.service.user.updateItemById(ctx.session._user._id, update) // 更新session await this.service.auth.updateSessionUser() data diff --git a/app/controller/setting.js b/app/controller/setting.js index d8e0634..47d01aa 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -26,10 +26,23 @@ module.exports = class SettingController extends Controller { if (ctx.query.filter) { select = ctx.query.filter } + let populate = null if (!ctx.session._isAuthed) { select = '-keys' + } else { + populate = [ + { + path: 'personal.user', + select: '-password' + } + ] } - const data = await this.service.setting.getItem({}, select) + const data = await this.service.setting.getItem( + {}, + select, + null, + populate + ) data ? ctx.success(data, '配置获取成功') : ctx.fail('配置获取失败') @@ -43,13 +56,25 @@ module.exports = class SettingController extends Controller { return ctx.fail('配置未找到') } const update = this.app.merge({}, exist, body) - await this.service.setting.updateItemById(exist._id, update) + let data = await this.service.setting.updateItemById( + exist._id, + update, + null, + [ + { + path: 'personal.user', + select: '-password' + } + ] + ) if (body.site && body.site.links) { // 抓取友链 - await this.service.setting.updateLinks() + data = await this.service.setting.updateLinks() + } + if (body.personal && body.personal.github) { + // 更新github信息 + data = await this.service.setting.updateGithubInfo() } - // 更新github信息 - const data = await this.service.setting.updateGithubInfo() data ? ctx.success(data, '配置更新成功') : ctx.fail('配置更新失败') diff --git a/app/extend/application.js b/app/extend/application.js index 04fbbb4..22a0b62 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -1,5 +1,6 @@ const mongoosePaginate = require('mongoose-paginate-v2') const lodash = require('lodash') +const merge = require('merge') const prefix = 'http://' @@ -34,7 +35,7 @@ module.exports = { return schema }, merge () { - return lodash.merge.apply(null, Array.prototype.slice.call(arguments)) + return merge.recursive.apply(null, [true].concat(Array.prototype.slice.call(arguments))) }, proxyUrl (url) { if (lodash.isString(url) && url.startsWith(prefix)) { diff --git a/app/model/notification.js b/app/model/notification.js index 42ffaf8..c669888 100644 --- a/app/model/notification.js +++ b/app/model/notification.js @@ -26,13 +26,13 @@ module.exports = app => { verb: { type: String, required: true, default: '' }, target: { // article user comment 根据情况是否包含 - article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article' }, - user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, - comment: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, + article: { type: Schema.Types.ObjectId, ref: 'Article' }, + user: { type: Schema.Types.ObjectId, ref: 'User' }, + comment: { type: Schema.Types.ObjectId, ref: 'Comment' }, }, actors: { - from: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, - to: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } + from: { type: Schema.Types.ObjectId, ref: 'User' }, + to: { type: Schema.Types.ObjectId, ref: 'User' } } }) diff --git a/app/model/setting.js b/app/model/setting.js index 5bab80c..2978d69 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -28,6 +28,7 @@ module.exports = app => { skill: [{ type: String }], location: { type: String, default: '' }, company: { type: String, default: '' }, + user: { type: Schema.Types.ObjectId, ref: 'User' }, github: { type: Object, default: {} } }, // 第三方插件的参数 diff --git a/app/service/setting.js b/app/service/setting.js index 77be0c4..9cbd3fc 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -19,12 +19,15 @@ module.exports = class SettingService extends ProxyService { return exist } // TIP: 这里不能用create,create如果不传model,是不会创建的 - const data = await this.newAndSave() + const model = new this.model() + model.personal.user = this.app._admin._id + const data = await model.save() if (data) { this.logger.info('Setting初始化成功') } else { this.logger.info('Setting初始化失败') } + this.mountToApp(data) return data } @@ -40,6 +43,7 @@ module.exports = class SettingService extends ProxyService { if (link) { const userInfo = await this.service.github.getUserInfo(link.github) if (userInfo) { + link.name = link.name || userInfo.name link.avatar = this.app.proxyUrl(userInfo.avatar_url) link.slogan = userInfo.bio link.site = link.site || userInfo.blog || userInfo.url diff --git a/config/config.prod.js b/config/config.prod.js index 32f1b0e..ce011e0 100644 --- a/config/config.prod.js +++ b/config/config.prod.js @@ -5,7 +5,10 @@ module.exports = () => { csrf: { headerName: 'x-csrf-token', cookieName: 'csrfToken' - } + }, + domainWhiteList: [ + '*.jooger.me' + ] } config.session = { diff --git a/package.json b/package.json index 0456dbc..0ffff03 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "koa-is-json": "^1.0.0", "lodash": "^4.17.10", "marked": "^0.5.0", + "merge": "^1.2.0", "moment": "^2.22.2", "mongoose": "5.2.8", "mongoose-paginate-v2": "^1.0.12", From d42640dcbb7a98796fa8969e0622df91069c304e Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 9 Sep 2018 23:26:15 +0800 Subject: [PATCH 148/208] update: unauthed can't get article content --- app/service/article.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/service/article.js b/app/service/article.js index ed34639..4832617 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -28,7 +28,9 @@ module.exports = class ArticleService extends ProxyService { state: this.config.modelEnum.article.state.optional.PUBLISH }, { $inc: { 'meta.pvs': 1 } - }, opt, populate) + }, Object.assign({}, opt, { + select: '-content' + }), populate) } else { data = await this.getItem({ _id: id }, opt, populate) } From 4b001c4eb43cabead9b5fd0530beddd929f2b290 Mon Sep 17 00:00:00 2001 From: Jooger Date: Tue, 11 Sep 2018 02:30:14 +0800 Subject: [PATCH 149/208] update: add notification unviewed count api --- app/controller/notification.js | 42 +++++++++++++++++++++++++++------- app/router/backend.js | 2 +- app/service/setting.js | 23 +++++++++---------- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/app/controller/notification.js b/app/controller/notification.js index fed772d..401c876 100644 --- a/app/controller/notification.js +++ b/app/controller/notification.js @@ -42,19 +42,24 @@ module.exports = class NotificationController extends Controller { populate: [ { path: 'target.article', - select: 'title description meta' + populate: [ + { + path: 'category' + }, { + path: 'tag' + } + ] }, { path: 'target.user', - select: 'name email role github' + select: '-password' }, { path: 'target.comment', - select: 'state spam type meta' }, { path: 'actors.from', - select: 'name email github' + select: '-password' }, { path: 'actors.to', - select: 'name email github' + select: '-password' } ] } @@ -64,10 +69,31 @@ module.exports = class NotificationController extends Controller { : ctx.fail('通告列表获取失败') } - async count () { + async unviewedCount () { const { ctx } = this - const count = await this.service.notification.count({ viewed: false }) - ctx.success(count, '未读通告数量获取成功') + const list = await this.service.notification.getList({ viewed: false }) + const notificationTypes = this.config.modelEnum.notification.type.optional + const data = list.reduce((map, item) => { + if (item.type === notificationTypes.GENERAL) { + map.general++ + } else if (item.type === notificationTypes.COMMENT) { + map.comment++ + } else if (item.type === notificationTypes.LIKE) { + map.like++ + } else if (item.type === notificationTypes.USER) { + map.user++ + } + return map + }, { + general: 0, + comment: 0, + like: 0, + user: 0 + }) + ctx.success({ + total: list.length, + counts: data + }, '未读通告数量获取成功') } async view () { diff --git a/app/router/backend.js b/app/router/backend.js index 7c7a15e..f080b35 100644 --- a/app/router/backend.js +++ b/app/router/backend.js @@ -54,7 +54,7 @@ module.exports = app => { // Notification backendRouter.get('/notifications', auth, controller.notification.list) - backendRouter.get('/notifications/count', auth, controller.notification.count) + backendRouter.get('/notifications/count/unviewed', auth, controller.notification.unviewedCount) backendRouter.patch('/notifications/view', auth, controller.notification.viewAll) backendRouter.patch('/notifications/:id/view', auth, controller.notification.view) backendRouter.delete('/notifications/:id', auth, controller.notification.delete) diff --git a/app/service/setting.js b/app/service/setting.js index 9cbd3fc..4123fa0 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -14,18 +14,17 @@ module.exports = class SettingService extends ProxyService { * @return {Setting} 配置数据 */ async seed () { - const exist = await this.getItem() - if (exist) { - return exist - } - // TIP: 这里不能用create,create如果不传model,是不会创建的 - const model = new this.model() - model.personal.user = this.app._admin._id - const data = await model.save() - if (data) { - this.logger.info('Setting初始化成功') - } else { - this.logger.info('Setting初始化失败') + let data = await this.getItem() + if (!data) { + // TIP: 这里不能用create,create如果不传model,是不会创建的 + const model = new this.model() + model.personal.user = this.app._admin._id + data = await model.save() + if (data) { + this.logger.info('Setting初始化成功') + } else { + this.logger.info('Setting初始化失败') + } } this.mountToApp(data) return data From 8fe7a96a73fdf04bd80b750f09f4a3640175ebd2 Mon Sep 17 00:00:00 2001 From: Jooger Date: Wed, 12 Sep 2018 01:07:14 +0800 Subject: [PATCH 150/208] fix: mount to app after setting been updated --- app/controller/setting.js | 9 ++++++--- app/service/setting.js | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/controller/setting.js b/app/controller/setting.js index 47d01aa..f5fc2c0 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -75,8 +75,11 @@ module.exports = class SettingController extends Controller { // 更新github信息 data = await this.service.setting.updateGithubInfo() } - data - ? ctx.success(data, '配置更新成功') - : ctx.fail('配置更新失败') + if (data) { + this.service.setting.mountToApp(data) + ctx.success(data, '配置更新成功') + } else { + ctx.fail('配置更新失败') + } } } diff --git a/app/service/setting.js b/app/service/setting.js index 4123fa0..12c3442 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -103,10 +103,12 @@ module.exports = class SettingService extends ProxyService { * @param {Setting} setting 配置 */ async mountToApp (setting) { + let msg = '配置挂载成功' if (!setting) { + msg = '配置更新成功' setting = await this.getItem() } this.app.setting = setting || null - this.logger.info('配置挂载成功') + this.logger.info(msg) } } From 0e719c46cfe722cb4b5956ad4196b39435a45e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 12 Sep 2018 13:30:07 +0800 Subject: [PATCH 151/208] feature: add dockerfile --- Dockerfile | 16 ++++++++++++++++ package.json | 5 +++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7ba929d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +## SEE: https://github.com/eggjs/egg/issues/1431 +FROM node:8.12.0-alpine + +RUN mkdir -p /usr/src/app + +WORKDIR /usr/src/app + +COPY package.json /usr/src/app/package.json + +RUN npm i --registry=https://registry.npm.taobao.org + +COPY . /usr/src/app + +EXPOSE 3002 + +CMD npm run docker diff --git a/package.json b/package.json index 0ffff03..6e71889 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,9 @@ "node": ">=8.9.0" }, "scripts": { - "start": "egg-scripts start --daemon --title=egg-server-node-server", - "stop": "egg-scripts stop --title=egg-server-node-server", + "start": "egg-scripts start --daemon --title=node-server", + "stop": "egg-scripts stop --title=node-server", + "docker": "egg-scripts start --title=node-server", "dev": "egg-bin dev", "debug": "egg-bin debug", "test": "npm run lint -- --fix && npm run test-local", From 43ae744a7625da0340ce8c08145023af367be987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 12 Sep 2018 16:53:55 +0800 Subject: [PATCH 152/208] update: add docker compose --- .dockerignore | 2 ++ config/config.default.js | 6 ++--- docker-compose.dev.yml | 34 +++++++++++++++++++++++++++ docker-compose.yml | 50 ++++++++++++++++++++++++++++++++++++++++ init.d/mongo/init.js | 14 +++++++++++ 5 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 init.d/mongo/init.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..be0443a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules/ +.git/ diff --git a/config/config.default.js b/config/config.default.js index 320ed2c..5fc6d1c 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -72,7 +72,7 @@ module.exports = appInfo => { // mongoose配置 config.mongoose = { - url: 'mongodb://127.0.0.1/node-server', + url: process.env.EGG_MONGODB_URL || 'mongodb://node-server:node-server@127.0.0.1:27017/node-server', options: { useNewUrlParser: true, poolSize: 20, @@ -89,13 +89,13 @@ module.exports = appInfo => { host: '127.0.0.1', port: 6379, db: 0, - password: appInfo.name + password: process.env.EGG_REDIS_PASSWORD || appInfo.name }, util: { host: '127.0.0.1', port: 6379, db: 1, - password: appInfo.name + password: process.env.EGG_REDIS_PASSWORD || appInfo.name } } } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..abc0185 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,34 @@ +version: "3" +services: + redis: + image: redis:4.0.11-alpine + command: redis-server --appendonly yes --requirepass node-server + volumes: + - egg-redis:/data + networks: + - docker-node-server + ports: + - 6379:6379 + + mongodb: + image: mongo:3.6.7 + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: mongodb + MONGO_INITDB_DATABASE: node-server + volumes: + - egg-mongo:/data/db + - ./init.d/mongo/:/docker-entrypoint-initdb.d + networks: + - docker-node-server + ports: + - 27017:27017 + +volumes: + egg-mongo: + egg-redis: + +networks: + docker-node-server: + driver: bridge \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..41a26ef --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,50 @@ +version: "3" +services: + node-server: + image: node-server:latest + environment: + NODE_ENV: production + EGG_SERVER_ENV: prod + EGG_MONGODB_URL: mongodb://node-server:node-server@mongodb:27017/node-server + EGG_REDIS_PASSWORD: node-server + depends_on: + - redis + - mongodb + networks: + - docker-node-server + ports: + - 3002:3002 + + redis: + image: redis:4.0.11-alpine + # appendonly 数据持久化 + command: redis-server --appendonly yes --requirepass node-server + volumes: + - egg-redis:/data + networks: + - docker-node-server + ports: + - 6379:6379 + + mongodb: + image: mongo:3.6.7 + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: mongodb + MONGO_INITDB_DATABASE: node-server + volumes: + - egg-mongo:/data/db + - ./init.d/mongo/:/docker-entrypoint-initdb.d + networks: + - docker-node-server + ports: + - 27017:27017 + +volumes: + egg-mongo: + egg-redis: + +networks: + docker-node-server: + driver: bridge \ No newline at end of file diff --git a/init.d/mongo/init.js b/init.d/mongo/init.js new file mode 100644 index 0000000..ef22216 --- /dev/null +++ b/init.d/mongo/init.js @@ -0,0 +1,14 @@ +/* eslint-disable */ + +/** + * 1. create custom user + * 2. create collection (Before MongoDB can save your new database, a collection name must also be specified at the time of creation.) + */ +db.createUser({ + user: 'node-server', + pwd: 'node-server', + roles: [{ + role: 'readWrite', + db: 'node-server' + }] +}) From 8b781c383f9ac0720d9c0c8549bbdc4102aed03b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 12 Sep 2018 16:56:56 +0800 Subject: [PATCH 153/208] chore: lint code --- app/controller/article.js | 2 +- app/controller/stat.js | 2 +- app/lib/plugin/egg-alinode/agent.js | 4 ---- app/router/frontend.js | 2 +- app/service/sentry.js | 2 +- app/service/stat.js | 2 +- package.json | 2 +- 7 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/controller/article.js b/app/controller/article.js index b802d2f..db87efb 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -176,7 +176,7 @@ module.exports = class ArticleController extends Controller { title: body.title }) this.logger.info(exist); - + if (exist) { return ctx.fail('文章名称重复') } diff --git a/app/controller/stat.js b/app/controller/stat.js index 009f367..fb1946d 100644 --- a/app/controller/stat.js +++ b/app/controller/stat.js @@ -11,7 +11,7 @@ module.exports = class StatController extends Controller { values: this.service.stat.dimensionsValidate, required: true }, - target: { type: 'string', required: true } + target: { type: 'string', required: true } } } } diff --git a/app/lib/plugin/egg-alinode/agent.js b/app/lib/plugin/egg-alinode/agent.js index e03a554..3957d6f 100644 --- a/app/lib/plugin/egg-alinode/agent.js +++ b/app/lib/plugin/egg-alinode/agent.js @@ -1,9 +1,5 @@ module.exports = agent => { agent.logger.info(333) agent.messenger.on('egg-ready', () => { - agent.logger.info(222) - agent.messenger.on('alinode-run', config => { - agent.logger.info(111) - }) }) } diff --git a/app/router/frontend.js b/app/router/frontend.js index 66c6e7b..7512c19 100644 --- a/app/router/frontend.js +++ b/app/router/frontend.js @@ -1,6 +1,6 @@ module.exports = app => { const fontendRouter = app.router.namespace('/v2') - const { router, controller } = app + const { controller } = app // Article fontendRouter.get('/articles', controller.article.list) diff --git a/app/service/sentry.js b/app/service/sentry.js index 0f747ed..8c7a429 100644 --- a/app/service/sentry.js +++ b/app/service/sentry.js @@ -1,4 +1,4 @@ -const { Service } = require('egg') +const { Service } = require('egg') module.exports = class SentryService extends Service { /** diff --git a/app/service/stat.js b/app/service/stat.js index fd9d951..13d88ce 100644 --- a/app/service/stat.js +++ b/app/service/stat.js @@ -107,7 +107,7 @@ module.exports = class StatService extends ProxyService { filter.type = this.statConfig.USER_CREATE } console.log(filter); - + return service && service.count(filter) || null } diff --git a/package.json b/package.json index 6e71889..d65abef 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "test": "npm run lint -- --fix && npm run test-local", "test-local": "egg-bin test", "cov": "egg-bin cov", - "lint": "eslint .", + "lint": "eslint . --fix", "ci": "npm run lint && npm run cov", "autod": "autod" }, From 201dec984c717bf975696835a94a7236c35e9c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 12 Sep 2018 23:16:18 +0800 Subject: [PATCH 154/208] update: update docker install from npm to yarn --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7ba929d..1d85619 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /usr/src/app COPY package.json /usr/src/app/package.json -RUN npm i --registry=https://registry.npm.taobao.org +RUN yarn install COPY . /usr/src/app From 91575c3d961a4ded184703b9ff6275deb41fc598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Thu, 13 Sep 2018 01:03:57 +0800 Subject: [PATCH 155/208] chore: update docker compose file --- Dockerfile | 2 ++ docker-compose.yml | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1d85619..4f4d140 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,8 @@ WORKDIR /usr/src/app COPY package.json /usr/src/app/package.json +RUN yarn config set registry 'https://registry.npm.taobao.org' + RUN yarn install COPY . /usr/src/app diff --git a/docker-compose.yml b/docker-compose.yml index 41a26ef..e50fe5f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,8 @@ version: "3" services: node-server: - image: node-server:latest + # 阿里云容器代理 + image: registry.cn-beijing.aliyuncs.com/jooger/node-server:latest environment: NODE_ENV: production EGG_SERVER_ENV: prod From 13abe20ac66214db89791fb7d534d4ad903b25f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Thu, 13 Sep 2018 14:53:36 +0800 Subject: [PATCH 156/208] update: docker add redis env --- config/config.default.js | 8 ++++---- docker-compose.yml | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/config/config.default.js b/config/config.default.js index 5fc6d1c..50eaf1b 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -86,14 +86,14 @@ module.exports = appInfo => { config.redis = { clients: { token: { - host: '127.0.0.1', - port: 6379, + host: process.env.EGG_REDIS_HOST || '127.0.0.1', + port: process.env.EGG_REDIS_PORT || 6379, db: 0, password: process.env.EGG_REDIS_PASSWORD || appInfo.name }, util: { - host: '127.0.0.1', - port: 6379, + host: process.env.EGG_REDIS_HOST || '127.0.0.1', + port: process.env.EGG_REDIS_PORT || 6379, db: 1, password: process.env.EGG_REDIS_PASSWORD || appInfo.name } diff --git a/docker-compose.yml b/docker-compose.yml index e50fe5f..1cc3187 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: NODE_ENV: production EGG_SERVER_ENV: prod EGG_MONGODB_URL: mongodb://node-server:node-server@mongodb:27017/node-server + EGG_REDIS_HOST: redis + EGG_REDIS_PORT: 6379 EGG_REDIS_PASSWORD: node-server depends_on: - redis @@ -24,8 +26,8 @@ services: - egg-redis:/data networks: - docker-node-server - ports: - - 6379:6379 + # ports: + # - 6379:6379 mongodb: image: mongo:3.6.7 @@ -39,8 +41,8 @@ services: - ./init.d/mongo/:/docker-entrypoint-initdb.d networks: - docker-node-server - ports: - - 27017:27017 + # ports: + # - 27017:27017 volumes: egg-mongo: From 9f87c2e6b08238bad05b3050d3ed96d7dc489d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Thu, 13 Sep 2018 16:00:13 +0800 Subject: [PATCH 157/208] chore: add release tag --- .release-it.json | 90 ++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 ++- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 .release-it.json diff --git a/.release-it.json b/.release-it.json new file mode 100644 index 0000000..83acbb1 --- /dev/null +++ b/.release-it.json @@ -0,0 +1,90 @@ +{ + "non-interactive": false, + "dry-run": false, + "verbose": false, + "force": false, + "pkgFiles": ["package.json"], + "increment": "patch", + "preReleaseId": null, + "buildCommand": false, + "safeBump": true, + "beforeChangelogCommand": false, + "changelogCommand": "git log --pretty=format:\"* %s (%h)\" [REV_RANGE]", + "requireCleanWorkingDir": true, + "requireUpstream": true, + "src": { + "commit": true, + "commitMessage": "[chore] release %s", + "commitArgs": "", + "tag": true, + "tagName": "release-v%s", + "tagAnnotation": "Release %s", + "push": true, + "pushArgs": "", + "pushRepo": "origin", + "beforeStartCommand": false, + "afterReleaseCommand": false, + "addUntrackedFiles": false + }, + "npm": { + "publish": false, + "publishPath": ".", + "tag": "latest", + "private": false, + "access": null, + "otp": null + }, + "github": { + "release": false, + "releaseName": "Release %s", + "preRelease": false, + "draft": false, + "tokenRef": "GITHUB_TOKEN", + "assets": null, + "host": null, + "timeout": 0, + "proxy": false + }, + "dist": { + "repo": false, + "stageDir": ".stage", + "baseDir": "dist", + "files": ["**/*"], + "pkgFiles": null, + "commit": true, + "commitMessage": "Release %s", + "commitArgs": "", + "tag": true, + "tagName": "%s", + "tagAnnotation": "Release %s", + "push": true, + "pushArgs": "", + "beforeStageCommand": false, + "afterReleaseCommand": false, + "addUntrackedFiles": false, + "github": { + "release": false + }, + "npm": { + "publish": false + } + }, + "prompt": { + "src": { + "status": false, + "commit": true, + "tag": true, + "push": true, + "release": true, + "publish": true + }, + "dist": { + "status": false, + "commit": true, + "tag": false, + "push": true, + "release": false, + "publish": false + } + } +} diff --git a/package.json b/package.json index d65abef..de73002 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "egg-mock": "^3.14.0", "eslint": "^4.11.0", "eslint-config-egg": "^6.0.0", + "release-it": "^7.6.1", "webstorm-disable-index": "^1.2.0" }, "engines": { @@ -58,7 +59,8 @@ "cov": "egg-bin cov", "lint": "eslint . --fix", "ci": "npm run lint && npm run cov", - "autod": "autod" + "autod": "autod", + "rc": "release-it" }, "ci": { "version": "8" From 6c3d04a7182e6ac9b8023d5244494ecef8b0672b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Thu, 13 Sep 2018 16:09:46 +0800 Subject: [PATCH 158/208] [chore] release 2.0.0-alpha.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de73002..1835cf3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "1.0.0", + "version": "2.0.0-alpha.0", "description": "", "private": true, "dependencies": { From 02ec3b28b0274b417d3958fcd4075d253f5569ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sat, 15 Sep 2018 01:10:36 +0800 Subject: [PATCH 159/208] update: user list add comment count --- app/controller/user.js | 9 ++++++++- app/service/user.js | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/controller/user.js b/app/controller/user.js index 6a7a697..02f9336 100644 --- a/app/controller/user.js +++ b/app/controller/user.js @@ -19,7 +19,14 @@ module.exports = class UserController extends Controller { if (!ctx.session._isAuthed) { select += ' -createdAt -updatedAt -role' } - const data = await this.service.user.getList({}, select) + const query = { + $nor: [ + { + role: this.config.modelEnum.user.role.optional.ADMIN + } + ] + } + const data = await this.service.user.getListWithComments(query, select) data ? ctx.success(data, '用户列表获取成功') : ctx.fail('用户列表获取失败') diff --git a/app/service/user.js b/app/service/user.js index 465d317..989187f 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -9,6 +9,24 @@ module.exports = class UserService extends ProxyService { return this.app.model.User } + async getListWithComments (query, select) { + let list = await this.getList(query, select, { + sort: '-createdAt' + }) + if (list && list.length) { + list = await Promise.all( + list.map(async item => { + item = item.toObject() + item.comments = await this.service.comment.count({ + author: item._id + }) + return item + }) + ) + } + return list + } + // 创建用户 async create (user) { const { name } = user From 5bbda864e028693f9f7c5194df1ba086afaf2d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 17 Sep 2018 12:53:04 +0800 Subject: [PATCH 160/208] fix: setting seed --- app/service/setting.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/service/setting.js b/app/service/setting.js index 12c3442..8d669c8 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -18,7 +18,9 @@ module.exports = class SettingService extends ProxyService { if (!data) { // TIP: 这里不能用create,create如果不传model,是不会创建的 const model = new this.model() - model.personal.user = this.app._admin._id + if (this.app._admin) { + model.personal.user = this.app._admin._id + } data = await model.save() if (data) { this.logger.info('Setting初始化成功') From a6104b6af3d36747fbfeafc157654bd7aa52ac0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 17 Sep 2018 12:53:46 +0800 Subject: [PATCH 161/208] [chore] release 2.0.0-alpha.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1835cf3..7bc08a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "2.0.0-alpha.0", + "version": "2.0.0-alpha.1", "description": "", "private": true, "dependencies": { From 9167b759abf121c30c172f8cc7e03b8070221e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 17 Sep 2018 17:29:44 +0800 Subject: [PATCH 162/208] chore: change port from 3002 to 7001 --- Dockerfile | 2 +- config/config.default.js | 12 ++++++------ docker-compose.yml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4f4d140..368df78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,6 @@ RUN yarn install COPY . /usr/src/app -EXPOSE 3002 +EXPOSE 7001 CMD npm run docker diff --git a/config/config.default.js b/config/config.default.js index 50eaf1b..a465e4a 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -1,12 +1,12 @@ module.exports = appInfo => { const config = exports = {} - config.cluster = { - listen: { - port: 3002, - hostname: '127.0.0.1' - } - } + // config.cluster = { + // listen: { + // port: 3002, + // hostname: '127.0.0.1' + // } + // } // use for cookie sign key, should change to your own and keep security config.keys = appInfo.name + '_1534765762288_2697' diff --git a/docker-compose.yml b/docker-compose.yml index 1cc3187..5a627c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: networks: - docker-node-server ports: - - 3002:3002 + - 7001:7001 redis: image: redis:4.0.11-alpine From 91073edf3fea2a04a7fd584bd6d1868de66fe4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 17 Sep 2018 17:42:08 +0800 Subject: [PATCH 163/208] [chore] release 2.0.0-alpha.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7bc08a9..82931df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "", "private": true, "dependencies": { From 9f7839a9d073ac8592bcd5efffa9e6264ab44be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 17 Sep 2018 23:04:52 +0800 Subject: [PATCH 164/208] update: crsf update --- app/service/auth.js | 20 ++++++++++++++++---- app/service/stat.js | 2 -- app/utils/token.js | 9 --------- config/config.default.js | 14 +++++++++++++- config/config.local.js | 9 --------- config/config.prod.js | 10 ---------- 6 files changed, 29 insertions(+), 35 deletions(-) delete mode 100644 app/utils/token.js diff --git a/app/service/auth.js b/app/service/auth.js index 80e481f..99766c3 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -2,23 +2,35 @@ * @desc Auth Services */ +const jwt = require('jsonwebtoken') const { Service } = require('egg') module.exports = class AuthService extends Service { + sign (app, payload = {}, isLogin = true) { + return jwt.sign(payload, app.config.secrets, { expiresIn: isLogin ? app.config.session.maxAge : 0 }) + } + /** * @desc 设置cookie,用于登录和退出 * @param {User} user 登录用户 * @param {Boolean} isLogin 是否是登录操作 * @return {String} token 用户token */ - setCookie (user, isLogin = false) { + setCookie (user, isLogin = true) { const { key, domain, maxAge, signed } = this.app.config.session - const token = this.app.utils.token.sign(this.app, { + const token = this.sign(this.app, { id: user._id, name: user.name }, isLogin) - this.ctx.cookies.set(key, token, { signed, domain, maxAge: isLogin ? maxAge : 0, httpOnly: false }) - this.ctx.cookies.set(this.app.config.userCookieKey, user._id, { signed, domain, maxAge: isLogin ? maxAge : 0, httpOnly: false }) + const payload = { + signed, + domain, + maxAge: + isLogin ? maxAge : 0, + httpOnly: false + } + this.ctx.cookies.set(key, token, payload) + this.ctx.cookies.set(this.app.config.userCookieKey, user._id, payload) return token } diff --git a/app/service/stat.js b/app/service/stat.js index 13d88ce..5a9bbfe 100644 --- a/app/service/stat.js +++ b/app/service/stat.js @@ -106,8 +106,6 @@ module.exports = class StatService extends ProxyService { service = this filter.type = this.statConfig.USER_CREATE } - console.log(filter); - return service && service.count(filter) || null } diff --git a/app/utils/token.js b/app/utils/token.js deleted file mode 100644 index 0f740be..0000000 --- a/app/utils/token.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @desc jwt sign token - */ - -const jwt = require('jsonwebtoken') - -exports.sign = (app, payload = {}, isLogin = true) => { - return jwt.sign(payload, app.config.secrets, { expiresIn: isLogin ? app.config.session.maxAge : 0 }) -} diff --git a/config/config.default.js b/config/config.default.js index a465e4a..a95ff33 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -27,6 +27,18 @@ module.exports = appInfo => { 'headers' ] + config.security = { + domainWhiteList: [ + '*.jooger.me', + 'jooger.me', + 'localhost:8081' + ], + csrf: { + enable: false + } + } + + config.cors = { enable: true, credentials: true, @@ -35,7 +47,7 @@ module.exports = appInfo => { config.session = { key: appInfo.name + '_token', - maxAge: 60000 * 60 * 24 * 7, + maxAge: 60 * 60 * 24 * 7, signed: true } diff --git a/config/config.local.js b/config/config.local.js index b66ad2e..028cfb4 100644 --- a/config/config.local.js +++ b/config/config.local.js @@ -8,15 +8,6 @@ module.exports = () => { consoleLevel: 'DEBUG', } - config.security = { - domainWhiteList: [ - '*' - ], - csrf: { - ignore: () => true - } - } - // 本地开发调试用 config.github = { clientId: '5b4d4a7945347d0fd2e2', diff --git a/config/config.prod.js b/config/config.prod.js index ce011e0..c84076a 100644 --- a/config/config.prod.js +++ b/config/config.prod.js @@ -1,16 +1,6 @@ module.exports = () => { const config = exports = {} - config.security = { - csrf: { - headerName: 'x-csrf-token', - cookieName: 'csrfToken' - }, - domainWhiteList: [ - '*.jooger.me' - ] - } - config.session = { domain: '.jooger.me' } From 563255d4e557ba70be9a7511f863c486e5ff7861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 17 Sep 2018 23:05:11 +0800 Subject: [PATCH 165/208] [chore] release 2.0.0-alpha.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 82931df..721213e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.3", "description": "", "private": true, "dependencies": { From 36be1f822c422281cf6786f17cb868bb92745dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Mon, 17 Sep 2018 23:15:26 +0800 Subject: [PATCH 166/208] update: update docker image mirror --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5a627c0..4694cd3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: "3" services: node-server: - # 阿里云容器代理 - image: registry.cn-beijing.aliyuncs.com/jooger/node-server:latest + # 阿里云容器代理(内网) + image: registry-internal.cn-beijing.aliyuncs.com/jooger/node-server:latest environment: NODE_ENV: production EGG_SERVER_ENV: prod From 50cfa0291c8e01308a3e6bd99bc2857454e6e2f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 18 Sep 2018 00:46:17 +0800 Subject: [PATCH 167/208] update: update docker image origin --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4694cd3..86aa7ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: node-server: # 阿里云容器代理(内网) - image: registry-internal.cn-beijing.aliyuncs.com/jooger/node-server:latest + image: registry.cn-beijing.aliyuncs.com/jooger/node-server:latest environment: NODE_ENV: production EGG_SERVER_ENV: prod From a2dd601fa1b6c6cf0af27f5d0e60c7fc7ab450ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 18 Sep 2018 11:43:25 +0800 Subject: [PATCH 168/208] fix: auth seed remove github related --- app/service/auth.js | 16 +++++----------- config/config.default.js | 6 ++++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/service/auth.js b/app/service/auth.js index 99766c3..7195cc0 100644 --- a/app/service/auth.js +++ b/app/service/auth.js @@ -42,17 +42,11 @@ module.exports = class AuthService extends Service { let admin = await this.service.user.getItem({ role: ADMIN }) if (!admin) { const defaultAdmin = this.config.defaultAdmin - const userInfo = await this.service.github.getUserInfo(defaultAdmin.name) - if (userInfo) { - admin = await this.service.user.create({ - role: ADMIN, - name: userInfo.name, - email: userInfo.email || this.config.author.email, - password: this.app.utils.encode.bhash(defaultAdmin.password), - site: userInfo.blog || userInfo.url, - avatar: this.app.proxyUrl(userInfo.avatar_url) - }) - } + admin = await this.service.user.create(Object.assign({}, defaultAdmin, { + role: ADMIN, + password: this.app.utils.encode.bhash(defaultAdmin.password), + avatar: this.app.utils.gravatar(defaultAdmin.email) + })) } // 挂载在session上 this.app._admin = admin diff --git a/config/config.default.js b/config/config.default.js index a95ff33..c84d5cc 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -229,8 +229,10 @@ module.exports = appInfo => { // 初始化管理员,默认的名称和密码,名称需要是github名称 config.defaultAdmin = { - name: appInfo.pkg.author.name, - password: 'admin123456' + name: 'Jooger', + password: 'admin123456', + email: 'iamjooger@gmail.com', + site: 'https://jooger.me' } config.defaultAvatar = 'https://static.jooger.me/img/common/avatar.png' From 6b7797976198447806f163a4153f69eafd008550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 18 Sep 2018 23:33:13 +0800 Subject: [PATCH 169/208] fix: session expiredTime changed --- config/config.default.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.default.js b/config/config.default.js index c84d5cc..1b27fd6 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -47,7 +47,7 @@ module.exports = appInfo => { config.session = { key: appInfo.name + '_token', - maxAge: 60 * 60 * 24 * 7, + maxAge: 1000 * 60 * 60 * 24 * 7, signed: true } From b9a74bbafb1c0006bfa292999587509d00f80c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 21 Sep 2018 02:30:58 +0800 Subject: [PATCH 170/208] fix: setting data populate update --- app/controller/setting.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/controller/setting.js b/app/controller/setting.js index f5fc2c0..ede8671 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -29,6 +29,12 @@ module.exports = class SettingController extends Controller { let populate = null if (!ctx.session._isAuthed) { select = '-keys' + populate = [ + { + path: 'personal.user', + select: 'name email site avatar' + } + ] } else { populate = [ { From b36bf7f0bd32bf13455cadeab5b7775c3247565a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 21 Sep 2018 23:55:38 +0800 Subject: [PATCH 171/208] chore: docker compose file update --- docker-compose.yml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 86aa7ab..ccc4ff5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,9 @@ version: "3" services: node-server: - # 阿里云容器代理(内网) + # 阿里云容器代理 image: registry.cn-beijing.aliyuncs.com/jooger/node-server:latest + # 环境变量 environment: NODE_ENV: production EGG_SERVER_ENV: prod @@ -10,25 +11,27 @@ services: EGG_REDIS_HOST: redis EGG_REDIS_PORT: 6379 EGG_REDIS_PASSWORD: node-server + # 依赖项,会在redis和mongo启动之后再启动 depends_on: - redis - mongodb networks: - docker-node-server + # 端口映射 ports: - 7001:7001 - + redis: image: redis:4.0.11-alpine # appendonly 数据持久化 command: redis-server --appendonly yes --requirepass node-server volumes: - egg-redis:/data - networks: + networks: - docker-node-server - # ports: - # - 6379:6379 - + ports: + - 6379:6379 + mongodb: image: mongo:3.6.7 restart: always @@ -39,10 +42,10 @@ services: volumes: - egg-mongo:/data/db - ./init.d/mongo/:/docker-entrypoint-initdb.d - networks: + networks: - docker-node-server - # ports: - # - 27017:27017 + ports: + - 27017:27017 volumes: egg-mongo: @@ -50,4 +53,4 @@ volumes: networks: docker-node-server: - driver: bridge \ No newline at end of file + driver: bridge From cdebf7e5a3e95181e9ec3934eebca2accd3db04a Mon Sep 17 00:00:00 2001 From: Jooger Date: Sun, 23 Sep 2018 22:53:14 +0800 Subject: [PATCH 172/208] fix: local config white domain list --- config/config.local.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/config.local.js b/config/config.local.js index 028cfb4..cfce9cd 100644 --- a/config/config.local.js +++ b/config/config.local.js @@ -3,6 +3,10 @@ module.exports = () => { const config = exports = {} + config.security = { + domainWhiteList: ['*'] + } + config.logger = { level: 'DEBUG', consoleLevel: 'DEBUG', From ca0de04828d0e9cf5701559d382b60655fa540f2 Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 24 Sep 2018 01:55:58 +0800 Subject: [PATCH 173/208] update: add article hot list api --- app/controller/article.js | 50 +++++++++++++++++++++++++++++++++++---- app/router/frontend.js | 1 + app/service/category.js | 2 +- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/app/controller/article.js b/app/controller/article.js index db87efb..3fe30c8 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -12,8 +12,8 @@ module.exports = class ArticleController extends Controller { limit: { type: 'int', required: false, min: 1 }, state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: false }, source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: false }, - category: { type: 'objectId', required: false }, - tag: { type: 'objectId', required: false }, + category: { type: 'string', required: false }, + tag: { type: 'string', required: false }, keyword: { type: 'string', required: false }, startDate: { type: 'string', required: false }, endDate: { type: 'string', required: false }, @@ -49,7 +49,7 @@ module.exports = class ArticleController extends Controller { } async list () { - const { ctx } = this + const { ctx, app } = this ctx.query.page = Number(ctx.query.page) const tranArray = ['limit', 'state', 'source', 'order'] tranArray.forEach(key => { @@ -77,7 +77,7 @@ module.exports = class ArticleController extends Controller { } ] } - const query = { state, category, tag, source } + const query = { state, source } // 搜索关键词 if (keyword) { @@ -87,6 +87,30 @@ module.exports = class ArticleController extends Controller { ] } + // 分类 + if (category) { + // 如果是id + if (app.utils.validate.isObjectId(category)) { + query.category = category + } else { + // 普通字符串,需要先查到id + const c = await this.service.category.getItem({ name: category }) + query.category = c ? c._id : app.utils.share.createObjectId() + } + } + + // 标签 + if (tag) { + // 如果是id + if (app.utils.validate.isObjectId(tag)) { + query.tag = tag + } else { + // 普通字符串,需要先查到id + const t = await this.service.tag.getItem({ name: tag }) + query.tag = t ? t._id : app.utils.share.createObjectId() + } + } + // 未通过权限校验(前台获取文章列表) if (!ctx.session._isAuthed) { // 将文章状态重置为1 @@ -242,4 +266,22 @@ module.exports = class ArticleController extends Controller { async archives () { this.ctx.success(await this.service.article.archives(), '归档获取成功') } + + async hot () { + const { ctx } = this + const limit = this.app.setting.limit.hotArticleCount + const data = await this.service.article.getList( + { + state: this.config.modelEnum.article.state.optional.PUBLISH + }, + '-content -renderedContent -state', + { + sort: '-meta.comments -meta.ups -meta.pvs', + limit + } + ) + data + ? ctx.success(data, '热门文章获取成功') + : ctx.fail('热门文章获取失败') + } } diff --git a/app/router/frontend.js b/app/router/frontend.js index 7512c19..610a1b5 100644 --- a/app/router/frontend.js +++ b/app/router/frontend.js @@ -5,6 +5,7 @@ module.exports = app => { // Article fontendRouter.get('/articles', controller.article.list) fontendRouter.get('/articles/archives', controller.article.archives) + fontendRouter.get('/articles/hot', controller.article.hot) fontendRouter.get('/articles/:id', controller.article.item) fontendRouter.patch('/articles/:id', controller.article.like) fontendRouter.patch('/articles/:id/like', controller.article.like) diff --git a/app/service/category.js b/app/service/category.js index 949d1c0..0082db4 100644 --- a/app/service/category.js +++ b/app/service/category.js @@ -11,7 +11,7 @@ module.exports = class CategoryService extends ProxyService { async getList (query, select = null, opt) { opt = this.app.merge({ - sort: '-createdAt' + sort: 'createdAt' }, opt) let categories = await this.model.find(query, select, opt).exec() if (categories.length) { From 64321767240415a1a36fc6e2e4295280864a0bcc Mon Sep 17 00:00:00 2001 From: Jooger Date: Mon, 24 Sep 2018 21:17:59 +0800 Subject: [PATCH 174/208] fix: fix some bug --- app/controller/article.js | 5 ++++- app/extend/application.js | 1 + app/model/article.js | 7 +++++-- app/model/comment.js | 2 +- app/utils/markdown.js | 3 ++- app/utils/validate.js | 11 +++++++++++ 6 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/controller/article.js b/app/controller/article.js index 3fe30c8..3d76fda 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -61,7 +61,6 @@ module.exports = class ArticleController extends Controller { const { page, limit, state, keyword, category, tag, source, order, sortBy, startDate, endDate } = ctx.query const options = { sort: { - updatedAt: -1, createdAt: -1 }, page, @@ -117,6 +116,10 @@ module.exports = class ArticleController extends Controller { query.state = 1 // 文章列表不需要content和state options.select = '-content -renderedContent -state' + options.sort = { + updatedAt: -1, + createdAt: -1 + } } else { // 排序 if (sortBy && order) { diff --git a/app/extend/application.js b/app/extend/application.js index 22a0b62..1fda584 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -1,6 +1,7 @@ const mongoosePaginate = require('mongoose-paginate-v2') const lodash = require('lodash') const merge = require('merge') +const { isEmptyObject } = require('../utils/validate') const prefix = 'http://' diff --git a/app/model/article.js b/app/model/article.js index 36dc30b..734f29f 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -60,12 +60,15 @@ module.exports = app => { // HACK: 这里this指向的是query,而不是这个model delete this._update.updatedAt const { content, state } = this._update - const find = await this.findOne() + const find = await this.model.findOne(this._conditions) if (find) { if (content && content !== find.content) { this._update.renderedContent = app.utils.markdown.render(content) } - if (['title', 'content'].some(key => this._update[key] !== find[key])) { + if (['title', 'content'].some(key => { + return this._update.hasOwnProperty(key) + && this._update[key] !== find[key] + })) { // 只有内容和标题不一样时才更新updatedAt this._update.updatedAt = Date.now() } diff --git a/app/model/comment.js b/app/model/comment.js index 233322f..dc0641d 100644 --- a/app/model/comment.js +++ b/app/model/comment.js @@ -61,7 +61,7 @@ module.exports = app => { async findOneAndUpdate () { delete this._update.updatedAt const { content } = this._update - const find = await this.findOne() + const find = await this.model.findOne(this._conditions) if (find) { if (content && content !== find.content) { this._update.renderedContent = app.utils.markdown.render(content) diff --git a/app/utils/markdown.js b/app/utils/markdown.js index b279bfd..ce848bf 100644 --- a/app/utils/markdown.js +++ b/app/utils/markdown.js @@ -68,7 +68,7 @@ renderer.code = function (code, lang) { '\n' } - return '
' +
@@ -83,6 +83,7 @@ marked.setOptions({
     sanitize: false,
     tables: true,
     breaks: true,
+    headerIds: true,
     smartLists: true,
     smartypants: true,
     highlight (code, lang) {
diff --git a/app/utils/validate.js b/app/utils/validate.js
index a76bbe3..e1162e3 100644
--- a/app/utils/validate.js
+++ b/app/utils/validate.js
@@ -8,6 +8,17 @@ Object.keys(lodash).forEach(key => {
     }
 })
 
+exports.isEmptyObject = obj => {
+    if (typeof obj !== 'object') {
+        return false
+    }
+    /* eslint-disable */
+    for (let key in obj) {
+        return false
+    }
+    return true
+}
+
 exports.isObjectId = (str = '') => mongoose.Types.ObjectId.isValid(str)
 
 Object.keys(validator).forEach(key => {

From a5389daf53b969d296de13c9af40c86c29056203 Mon Sep 17 00:00:00 2001
From: Jooger 
Date: Fri, 28 Sep 2018 23:46:27 +0800
Subject: [PATCH 175/208] update: update markdown render

---
 app/utils/markdown.js | 52 ++++++++++++++++++++++++++++++-------------
 1 file changed, 37 insertions(+), 15 deletions(-)

diff --git a/app/utils/markdown.js b/app/utils/markdown.js
index ce848bf..eabcc6c 100644
--- a/app/utils/markdown.js
+++ b/app/utils/markdown.js
@@ -51,30 +51,52 @@ renderer.image = function (href, title, text) {
 	`.replace(/\s+/g, ' ').replace('\n', '')
 }
 
-renderer.code = function (code, lang) {
+renderer.code = function (code, lang, escaped) {
     if (this.options.highlight) {
-        const out = this.options.highlight(code, lang)
+        const out = this.options.highlight(code, lang);
         if (out != null && out !== code) {
-            code = out
+            escaped = true;
+            code = out;
         }
     }
 
-    const lineCode = code.split('\n')
-    const codeWrapper = lineCode.map((line, index) => `${line}${index !== lineCode.length - 1 ? '
' : ''}`.replace(/\s+/g, ' ')).join('') - if (!lang) { - return '
' +
-            codeWrapper +
-            '\n
' + return '
'
+          + (escaped ? code : escape(code, true))
+          + '
'; } - return '
' +
-        codeWrapper +
-        '\n
\n' + return '
'
+        + (escaped ? code : escape(code, true))
+        + '
\n'; } +// renderer.code = function (code, lang) { +// if (this.options.highlight) { +// const out = this.options.highlight(code, lang) +// if (out != null && out !== code) { +// code = out +// } +// } + +// const lineCode = code.split('\n') +// const codeWrapper = lineCode.map((line, index) => `${line}${index !== lineCode.length - 1 ? '
' : ''}`.replace(/\s+/g, ' ')).join('') + +// if (!lang) { +// return '
' +
+//             codeWrapper +
+//             '\n
' +// } + +// return '
' +
+//         codeWrapper +
+//         '\n
\n' +// } marked.setOptions({ renderer, From ca278c92f5d525802e21ca5f05a9ba751dbf6070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 30 Sep 2018 13:53:38 +0800 Subject: [PATCH 176/208] update: change mongo and redis port --- config/config.default.js | 6 +++--- docker-compose.dev.yml | 12 ++++++------ docker-compose.yml | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/config/config.default.js b/config/config.default.js index 1b27fd6..645f904 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -84,7 +84,7 @@ module.exports = appInfo => { // mongoose配置 config.mongoose = { - url: process.env.EGG_MONGODB_URL || 'mongodb://node-server:node-server@127.0.0.1:27017/node-server', + url: process.env.EGG_MONGODB_URL || 'mongodb://node-server:node-server@127.0.0.1:27016/node-server', options: { useNewUrlParser: true, poolSize: 20, @@ -99,13 +99,13 @@ module.exports = appInfo => { clients: { token: { host: process.env.EGG_REDIS_HOST || '127.0.0.1', - port: process.env.EGG_REDIS_PORT || 6379, + port: process.env.EGG_REDIS_PORT || 6378, db: 0, password: process.env.EGG_REDIS_PASSWORD || appInfo.name }, util: { host: process.env.EGG_REDIS_HOST || '127.0.0.1', - port: process.env.EGG_REDIS_PORT || 6379, + port: process.env.EGG_REDIS_PORT || 6378, db: 1, password: process.env.EGG_REDIS_PASSWORD || appInfo.name } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index abc0185..8f5243f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -5,11 +5,11 @@ services: command: redis-server --appendonly yes --requirepass node-server volumes: - egg-redis:/data - networks: + networks: - docker-node-server ports: - - 6379:6379 - + - 6378:6378 + mongodb: image: mongo:3.6.7 restart: always @@ -20,10 +20,10 @@ services: volumes: - egg-mongo:/data/db - ./init.d/mongo/:/docker-entrypoint-initdb.d - networks: + networks: - docker-node-server ports: - - 27017:27017 + - 27016:27016 volumes: egg-mongo: @@ -31,4 +31,4 @@ volumes: networks: docker-node-server: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index ccc4ff5..0871a18 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,9 +7,9 @@ services: environment: NODE_ENV: production EGG_SERVER_ENV: prod - EGG_MONGODB_URL: mongodb://node-server:node-server@mongodb:27017/node-server + EGG_MONGODB_URL: mongodb://node-server:node-server@mongodb:27016/node-server EGG_REDIS_HOST: redis - EGG_REDIS_PORT: 6379 + EGG_REDIS_PORT: 6378 EGG_REDIS_PASSWORD: node-server # 依赖项,会在redis和mongo启动之后再启动 depends_on: @@ -30,7 +30,7 @@ services: networks: - docker-node-server ports: - - 6379:6379 + - 6378:6378 mongodb: image: mongo:3.6.7 @@ -45,7 +45,7 @@ services: networks: - docker-node-server ports: - - 27017:27017 + - 27016:27016 volumes: egg-mongo: From 96e57200c07fac6c7c095cc663f0c8b210931861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 30 Sep 2018 14:14:54 +0800 Subject: [PATCH 177/208] update: article model add "from" schema --- app/controller/article.js | 21 +++++++++++++++------ app/model/article.js | 2 ++ docker-compose.dev.yml | 4 ++-- docker-compose.yml | 4 ++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/controller/article.js b/app/controller/article.js index 3d76fda..e80f9f2 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -6,12 +6,13 @@ const { Controller } = require('egg') module.exports = class ArticleController extends Controller { get rules () { + const { state, source } = this.config.modelEnum.article return { list: { page: { type: 'int', required: true, min: 1 }, limit: { type: 'int', required: false, min: 1 }, - state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: false }, - source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: false }, + state: { type: 'enum', values: Object.values(state.optional), required: false }, + source: { type: 'enum', values: Object.values(source.optional), required: false }, category: { type: 'string', required: false }, tag: { type: 'string', required: false }, keyword: { type: 'string', required: false }, @@ -28,8 +29,9 @@ module.exports = class ArticleController extends Controller { keywords: { type: 'array', required: false }, category: { type: 'objectId', required: true }, tag: { type: 'array', required: false, itemType: 'objectId' }, - state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: true }, - source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: true }, + state: { type: 'enum', values: Object.values(state.optional), required: true }, + source: { type: 'enum', values: Object.values(source.optional), required: true }, + from: { type: 'url', required: false }, thumb: { type: 'url', required: false }, createdAt: { type: 'string', required: false } }, @@ -40,8 +42,9 @@ module.exports = class ArticleController extends Controller { keywords: { type: 'array', required: false }, category: { type: 'objectId', required: false }, tag: { type: 'array', required: false, itemType: 'objectId' }, - state: { type: 'enum', values: Object.values(this.config.modelEnum.article.state.optional), required: false }, - source: { type: 'enum', values: Object.values(this.config.modelEnum.article.source.optional), required: false }, + state: { type: 'enum', values: Object.values(state.optional), required: false }, + source: { type: 'enum', values: Object.values(source.optional), required: false }, + from: { type: 'url', required: false }, thumb: { type: 'url', required: false }, createdAt: { type: 'string', required: false } } @@ -176,6 +179,9 @@ module.exports = class ArticleController extends Controller { async create () { const { ctx } = this const body = ctx.validateBody(this.rules.create) + if (body.source === this.config.modelEnum.article.source.optional.REPRINT && !body.from) { + return ctx.fail(422, '转载文章缺少原链接') + } if (body.createdAt) { body.createdAt = new Date(body.createdAt) } @@ -193,6 +199,9 @@ module.exports = class ArticleController extends Controller { const { ctx } = this const params = ctx.validateParamsObjectId() const body = ctx.validateBody(this.rules.update) + if (body.source === this.config.modelEnum.article.source.optional.REPRINT && !body.from) { + return ctx.fail(422, '转载文章缺少原链接') + } if (body.createdAt) { body.createdAt = new Date(body.createdAt) } diff --git a/app/model/article.js b/app/model/article.js index 734f29f..72a6ce9 100644 --- a/app/model/article.js +++ b/app/model/article.js @@ -30,6 +30,8 @@ module.exports = app => { default: articleValidateConfig.source.default, validate: val => Object.values(articleValidateConfig.source.optional).includes(val) }, + // source为1的时候的原文链接 + from: { type: String, validate: app.utils.validate.isUrl }, // 文章状态 ( 0 草稿 | 1 已发布 ) state: { type: Number, diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8f5243f..b095125 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -8,7 +8,7 @@ services: networks: - docker-node-server ports: - - 6378:6378 + - 6378:6379 mongodb: image: mongo:3.6.7 @@ -23,7 +23,7 @@ services: networks: - docker-node-server ports: - - 27016:27016 + - 27016:27017 volumes: egg-mongo: diff --git a/docker-compose.yml b/docker-compose.yml index 0871a18..c93cf02 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,7 @@ services: networks: - docker-node-server ports: - - 6378:6378 + - 6378:6379 mongodb: image: mongo:3.6.7 @@ -45,7 +45,7 @@ services: networks: - docker-node-server ports: - - 27016:27016 + - 27016:27017 volumes: egg-mongo: From c8910c7a7e8aef60404c1eeb340968ed2fe0f5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 30 Sep 2018 15:07:25 +0800 Subject: [PATCH 178/208] fix: fix docker container link port --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c93cf02..242e3e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,9 +7,9 @@ services: environment: NODE_ENV: production EGG_SERVER_ENV: prod - EGG_MONGODB_URL: mongodb://node-server:node-server@mongodb:27016/node-server + EGG_MONGODB_URL: mongodb://node-server:node-server@mongodb:27017/node-server EGG_REDIS_HOST: redis - EGG_REDIS_PORT: 6378 + EGG_REDIS_PORT: 6379 EGG_REDIS_PASSWORD: node-server # 依赖项,会在redis和mongo启动之后再启动 depends_on: From ba120e43ab255a216677547f7f889b4d2cdd6831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 30 Sep 2018 20:35:44 +0800 Subject: [PATCH 179/208] update: setting'site add logo option --- .gitignore | 1 - app/model/setting.js | 1 + yarn.lock | 7799 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 7800 insertions(+), 1 deletion(-) create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index 77b502a..d07e5df 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ npm-debug.log yarn-error.log node_modules/ package-lock.json -yarn.lock coverage/ .idea/ run/ diff --git a/app/model/setting.js b/app/model/setting.js index 2978d69..12e92e5 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -9,6 +9,7 @@ module.exports = app => { const SettingSchema = new Schema({ // 站点设置 site: { + logo: { type: String, validate: app.utils.validate.isUrl }, welcome: { type: String, default: '' }, links: [{ name: { type: String, required: true }, diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..09475a9 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,7799 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npm.taobao.org/@babel/code-frame/download/@babel/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9" + dependencies: + "@babel/highlight" "7.0.0-beta.44" + +"@babel/code-frame@7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "http://registry.npm.taobao.org/@babel/code-frame/download/@babel/code-frame-7.0.0-beta.51.tgz#bd71d9b192af978df915829d39d4094456439a0c" + dependencies: + "@babel/highlight" "7.0.0-beta.51" + +"@babel/generator@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npm.taobao.org/@babel/generator/download/@babel/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42" + dependencies: + "@babel/types" "7.0.0-beta.44" + jsesc "^2.5.1" + lodash "^4.2.0" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/generator@7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "http://registry.npm.taobao.org/@babel/generator/download/@babel/generator-7.0.0-beta.51.tgz#6c7575ffde761d07485e04baedc0392c6d9e30f6" + dependencies: + "@babel/types" "7.0.0-beta.51" + jsesc "^2.5.1" + lodash "^4.17.5" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/helper-function-name@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npm.taobao.org/@babel/helper-function-name/download/@babel/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd" + dependencies: + "@babel/helper-get-function-arity" "7.0.0-beta.44" + "@babel/template" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-function-name@7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "http://registry.npm.taobao.org/@babel/helper-function-name/download/@babel/helper-function-name-7.0.0-beta.51.tgz#21b4874a227cf99ecafcc30a90302da5a2640561" + dependencies: + "@babel/helper-get-function-arity" "7.0.0-beta.51" + "@babel/template" "7.0.0-beta.51" + "@babel/types" "7.0.0-beta.51" + +"@babel/helper-get-function-arity@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npm.taobao.org/@babel/helper-get-function-arity/download/@babel/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15" + dependencies: + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-get-function-arity@7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "http://registry.npm.taobao.org/@babel/helper-get-function-arity/download/@babel/helper-get-function-arity-7.0.0-beta.51.tgz#3281b2d045af95c172ce91b20825d85ea4676411" + dependencies: + "@babel/types" "7.0.0-beta.51" + +"@babel/helper-split-export-declaration@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npm.taobao.org/@babel/helper-split-export-declaration/download/@babel/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc" + dependencies: + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-split-export-declaration@7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "http://registry.npm.taobao.org/@babel/helper-split-export-declaration/download/@babel/helper-split-export-declaration-7.0.0-beta.51.tgz#8a6c3f66c4d265352fc077484f9f6e80a51ab978" + dependencies: + "@babel/types" "7.0.0-beta.51" + +"@babel/highlight@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npm.taobao.org/@babel/highlight/download/@babel/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +"@babel/highlight@7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "http://registry.npm.taobao.org/@babel/highlight/download/@babel/highlight-7.0.0-beta.51.tgz#e8844ae25a1595ccfd42b89623b4376ca06d225d" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +"@babel/parser@7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "http://registry.npm.taobao.org/@babel/parser/download/@babel/parser-7.0.0-beta.51.tgz#27cec2df409df60af58270ed8f6aa55409ea86f6" + +"@babel/template@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npm.taobao.org/@babel/template/download/@babel/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + lodash "^4.2.0" + +"@babel/template@7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "http://registry.npm.taobao.org/@babel/template/download/@babel/template-7.0.0-beta.51.tgz#9602a40aebcf357ae9677e2532ef5fc810f5fbff" + dependencies: + "@babel/code-frame" "7.0.0-beta.51" + "@babel/parser" "7.0.0-beta.51" + "@babel/types" "7.0.0-beta.51" + lodash "^4.17.5" + +"@babel/traverse@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npm.taobao.org/@babel/traverse/download/@babel/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966" + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/generator" "7.0.0-beta.44" + "@babel/helper-function-name" "7.0.0-beta.44" + "@babel/helper-split-export-declaration" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + debug "^3.1.0" + globals "^11.1.0" + invariant "^2.2.0" + lodash "^4.2.0" + +"@babel/traverse@7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "http://registry.npm.taobao.org/@babel/traverse/download/@babel/traverse-7.0.0-beta.51.tgz#981daf2cec347a6231d3aa1d9e1803b03aaaa4a8" + dependencies: + "@babel/code-frame" "7.0.0-beta.51" + "@babel/generator" "7.0.0-beta.51" + "@babel/helper-function-name" "7.0.0-beta.51" + "@babel/helper-split-export-declaration" "7.0.0-beta.51" + "@babel/parser" "7.0.0-beta.51" + "@babel/types" "7.0.0-beta.51" + debug "^3.1.0" + globals "^11.1.0" + invariant "^2.2.0" + lodash "^4.17.5" + +"@babel/types@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npm.taobao.org/@babel/types/download/@babel/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757" + dependencies: + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^2.0.0" + +"@babel/types@7.0.0-beta.51": + version "7.0.0-beta.51" + resolved "http://registry.npm.taobao.org/@babel/types/download/@babel/types-7.0.0-beta.51.tgz#d802b7b543b5836c778aa691797abf00f3d97ea9" + dependencies: + esutils "^2.0.2" + lodash "^4.17.5" + to-fast-properties "^2.0.0" + +"@gimenete/type-writer@^0.1.3": + version "0.1.3" + resolved "http://registry.npm.taobao.org/@gimenete/type-writer/download/@gimenete/type-writer-0.1.3.tgz#2d4f26118b18d71f5b34ca24fdd6d1fd455c05b6" + dependencies: + camelcase "^5.0.0" + prettier "^1.13.7" + +"@koa/cors@^2.2.1": + version "2.2.2" + resolved "http://registry.npm.taobao.org/@koa/cors/download/@koa/cors-2.2.2.tgz#9084ab7f58107734e6b19d602d99538eda73f2d0" + +"@mrmlnc/readdir-enhanced@^2.2.1": + version "2.2.1" + resolved "http://registry.npm.taobao.org/@mrmlnc/readdir-enhanced/download/@mrmlnc/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" + dependencies: + call-me-maybe "^1.0.1" + glob-to-regexp "^0.3.0" + +"@nodelib/fs.stat@^1.0.1": + version "1.1.2" + resolved "http://registry.npm.taobao.org/@nodelib/fs.stat/download/@nodelib/fs.stat-1.1.2.tgz#54c5a964462be3d4d78af631363c18d6fa91ac26" + +"@octokit/rest@15.10.0": + version "15.10.0" + resolved "http://registry.npm.taobao.org/@octokit/rest/download/@octokit/rest-15.10.0.tgz#9baf7430e55edf1a1024c35ae72ed2f5fc6e90e9" + dependencies: + "@gimenete/type-writer" "^0.1.3" + before-after-hook "^1.1.0" + btoa-lite "^1.0.0" + debug "^3.1.0" + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.0" + lodash "^4.17.4" + node-fetch "^2.1.1" + url-template "^2.0.8" + +"@sindresorhus/is@^0.7.0": + version "0.7.0" + resolved "http://registry.npm.taobao.org/@sindresorhus/is/download/@sindresorhus/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" + +"@types/accepts@*", "@types/accepts@^1.3.5": + version "1.3.5" + resolved "http://registry.npm.taobao.org/@types/accepts/download/@types/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" + dependencies: + "@types/node" "*" + +"@types/body-parser@*": + version "1.17.0" + resolved "http://registry.npm.taobao.org/@types/body-parser/download/@types/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.32" + resolved "http://registry.npm.taobao.org/@types/connect/download/@types/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" + dependencies: + "@types/node" "*" + +"@types/cookies@*": + version "0.7.1" + resolved "http://registry.npm.taobao.org/@types/cookies/download/@types/cookies-0.7.1.tgz#f9f204bd6767d389eea3b87609e30c090c77a540" + dependencies: + "@types/connect" "*" + "@types/express" "*" + "@types/keygrip" "*" + "@types/node" "*" + +"@types/empower@*": + version "1.2.30" + resolved "http://registry.npm.taobao.org/@types/empower/download/@types/empower-1.2.30.tgz#c7cfc14b3a61e54c74c674c1fbc91ba2df0d1392" + +"@types/events@*": + version "1.2.0" + resolved "http://registry.npm.taobao.org/@types/events/download/@types/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" + +"@types/express-serve-static-core@*": + version "4.16.0" + resolved "http://registry.npm.taobao.org/@types/express-serve-static-core/download/@types/express-serve-static-core-4.16.0.tgz#fdfe777594ddc1fe8eb8eccce52e261b496e43e7" + dependencies: + "@types/events" "*" + "@types/node" "*" + "@types/range-parser" "*" + +"@types/express@*": + version "4.16.0" + resolved "http://registry.npm.taobao.org/@types/express/download/@types/express-4.16.0.tgz#6d8bc42ccaa6f35cf29a2b7c3333cb47b5a32a19" + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/http-assert@*": + version "1.3.0" + resolved "http://registry.npm.taobao.org/@types/http-assert/download/@types/http-assert-1.3.0.tgz#5e932606153da28e1d04f9043f4912cf61fd55dd" + +"@types/keygrip@*": + version "1.0.1" + resolved "http://registry.npm.taobao.org/@types/keygrip/download/@types/keygrip-1.0.1.tgz#ff540462d2fb4d0a88441ceaf27d287b01c3d878" + +"@types/koa-compose@*": + version "3.2.2" + resolved "http://registry.npm.taobao.org/@types/koa-compose/download/@types/koa-compose-3.2.2.tgz#dc106e000bbf92a3ac900f756df47344887ee847" + +"@types/koa-router@^7.0.31": + version "7.0.31" + resolved "http://registry.npm.taobao.org/@types/koa-router/download/@types/koa-router-7.0.31.tgz#afe279634cca7b4536be38cf641f35f58a4bc5ee" + dependencies: + "@types/koa" "*" + +"@types/koa@*", "@types/koa@^2.0.46": + version "2.0.46" + resolved "http://registry.npm.taobao.org/@types/koa/download/@types/koa-2.0.46.tgz#24bc3cd405d10fcde81f876cd8285b44d4ddc3e9" + dependencies: + "@types/accepts" "*" + "@types/cookies" "*" + "@types/events" "*" + "@types/http-assert" "*" + "@types/keygrip" "*" + "@types/koa-compose" "*" + "@types/node" "*" + +"@types/mime@*": + version "2.0.0" + resolved "http://registry.npm.taobao.org/@types/mime/download/@types/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" + +"@types/node@*": + version "10.10.1" + resolved "http://registry.npm.taobao.org/@types/node/download/@types/node-10.10.1.tgz#d5c96ca246a418404914d180b7fdd625ad18eca6" + +"@types/power-assert-formatter@*": + version "1.4.28" + resolved "http://registry.npm.taobao.org/@types/power-assert-formatter/download/@types/power-assert-formatter-1.4.28.tgz#25b8fddb6322259c6b91c35338d39b0f8e524252" + +"@types/power-assert@^1.5.0": + version "1.5.0" + resolved "http://registry.npm.taobao.org/@types/power-assert/download/@types/power-assert-1.5.0.tgz#4cc43717127cd81901555f905c55f02938120dcb" + dependencies: + "@types/empower" "*" + "@types/power-assert-formatter" "*" + +"@types/range-parser@*": + version "1.2.2" + resolved "http://registry.npm.taobao.org/@types/range-parser/download/@types/range-parser-1.2.2.tgz#fa8e1ad1d474688a757140c91de6dace6f4abc8d" + +"@types/serve-static@*": + version "1.13.2" + resolved "http://registry.npm.taobao.org/@types/serve-static/download/@types/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48" + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +"@types/urllib@^2.28.0": + version "2.28.0" + resolved "http://registry.npm.taobao.org/@types/urllib/download/@types/urllib-2.28.0.tgz#5c84e11f84d047409fac6cc5b91176f4d52c2dd0" + dependencies: + "@types/events" "*" + "@types/node" "*" + +JSONStream@^1.0.4: + version "1.3.4" + resolved "http://registry.npm.taobao.org/JSONStream/download/JSONStream-1.3.4.tgz#615bb2adb0cd34c8f4c447b5f6512fa1d8f16a2e" + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +a-sync-waterfall@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/a-sync-waterfall/download/a-sync-waterfall-1.0.1.tgz#75b6b6aa72598b497a125e7a2770f14f4c8a1fa7" + +abbrev@1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/abbrev/download/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +accepts@^1.3.5: + version "1.3.5" + resolved "http://registry.npm.taobao.org/accepts/download/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" + dependencies: + mime-types "~2.1.18" + negotiator "0.6.1" + +acorn-es7-plugin@^1.0.10, acorn-es7-plugin@^1.0.12: + version "1.1.7" + resolved "http://registry.npm.taobao.org/acorn-es7-plugin/download/acorn-es7-plugin-1.1.7.tgz#f2ee1f3228a90eead1245f9ab1922eb2e71d336b" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "http://registry.npm.taobao.org/acorn-jsx/download/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4: + version "3.3.0" + resolved "http://registry.npm.taobao.org/acorn/download/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^5.0.0, acorn@^5.5.0: + version "5.7.3" + resolved "http://registry.npm.taobao.org/acorn/download/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + +address@>=0.0.1, address@^1.0.0, address@^1.0.1: + version "1.0.3" + resolved "http://registry.npm.taobao.org/address/download/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" + +agent-base@4, agent-base@^4.1.0, agent-base@^4.2.0: + version "4.2.1" + resolved "http://registry.npm.taobao.org/agent-base/download/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + dependencies: + es6-promisify "^5.0.0" + +agentkeepalive@^3.4.1, agentkeepalive@^3.5.1: + version "3.5.1" + resolved "http://registry.npm.taobao.org/agentkeepalive/download/agentkeepalive-3.5.1.tgz#4eba75cf2ad258fc09efd506cdb8d8c2971d35a4" + dependencies: + humanize-ms "^1.2.1" + +agentx@^1.9.11: + version "1.9.11" + resolved "http://registry.npm.taobao.org/agentx/download/agentx-1.9.11.tgz#1058138977a53d814d975791a2f5b5afc3c3f44b" + dependencies: + debug "^3.1.0" + nounou "^1.2.1" + split2 "^2.2.0" + through2 "^2.0.3" + ws "^1.1.5" + +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "http://registry.npm.taobao.org/ajv-keywords/download/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + +ajv@^5.2.3, ajv@^5.3.0: + version "5.5.2" + resolved "http://registry.npm.taobao.org/ajv/download/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +akismet-api@^4.2.0: + version "4.2.0" + resolved "http://registry.npm.taobao.org/akismet-api/download/akismet-api-4.2.0.tgz#d3c1ca68cce97c2b075858de79b7aba4e7b2d9a1" + dependencies: + bluebird "^3.1.1" + superagent "^3.8.0" + +ali-oss@^4.10.1: + version "4.15.1" + resolved "http://registry.npm.taobao.org/ali-oss/download/ali-oss-4.15.1.tgz#e64a7aa9ddfca4573f8c143a59f55dddb1f49305" + dependencies: + address "^1.0.0" + agentkeepalive "^3.4.1" + any-promise "^1.3.0" + bowser "^1.6.0" + co "^4.6.0" + co-defer "^1.0.0" + co-gather "^0.0.1" + co-priority-queue "^1.0.3" + copy-to "^2.0.1" + dateformat "^2.0.0" + debug "^2.2.0" + destroy "^1.0.4" + end-or-error "^1.0.1" + get-ready "^1.0.0" + humanize-ms "^1.2.0" + is-type-of "^1.0.0" + jstoxml "^0.2.3" + merge-descriptors "^1.0.1" + mime "^1.3.4" + platform "^1.3.1" + sdk-base "^2.0.1" + stream-http "^2.8.0" + stream-wormhole "^1.0.4" + urllib "^2.17.1" + utility "^1.8.0" + xml2js "^0.4.16" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "http://registry.npm.taobao.org/amdefine/download/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-align@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/ansi-align/download/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" + dependencies: + string-width "^2.0.0" + +ansi-escapes@^3.0.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/ansi-escapes/download/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "http://registry.npm.taobao.org/ansi-styles/download/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "http://registry.npm.taobao.org/ansi-styles/download/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + dependencies: + color-convert "^1.9.0" + +any-promise@^1.0.0, any-promise@^1.1.0, any-promise@^1.3.0: + version "1.3.0" + resolved "http://registry.npm.taobao.org/any-promise/download/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + +anymatch@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/anymatch/download/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +append-transform@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/append-transform/download/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" + dependencies: + default-require-extensions "^2.0.0" + +aproba@^1.0.3: + version "1.2.0" + resolved "http://registry.npm.taobao.org/aproba/download/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +archy@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/archy/download/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "http://registry.npm.taobao.org/are-we-there-yet/download/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.10" + resolved "http://registry.npm.taobao.org/argparse/download/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + dependencies: + sprintf-js "~1.0.2" + +aria-query@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/aria-query/download/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" + dependencies: + ast-types-flow "0.0.7" + commander "^2.11.0" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/arr-diff/download/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/arr-flatten/download/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +arr-union@^3.1.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/arr-union/download/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + +array-differ@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/array-differ/download/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" + +array-filter@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/array-filter/download/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "http://registry.npm.taobao.org/array-find-index/download/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-find@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/array-find/download/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8" + +array-ify@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/array-ify/download/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" + +array-includes@^3.0.3: + version "3.0.3" + resolved "http://registry.npm.taobao.org/array-includes/download/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + +array-union@^1.0.1: + version "1.0.2" + resolved "http://registry.npm.taobao.org/array-union/download/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "http://registry.npm.taobao.org/array-uniq/download/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.3.2: + version "0.3.2" + resolved "http://registry.npm.taobao.org/array-unique/download/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + +arrify@^1.0.0, arrify@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/arrify/download/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asap@^2.0.3: + version "2.0.6" + resolved "http://registry.npm.taobao.org/asap/download/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/assign-symbols/download/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + +ast-types-flow@0.0.7, ast-types-flow@^0.0.7: + version "0.0.7" + resolved "http://registry.npm.taobao.org/ast-types-flow/download/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + +ast-types@0.x.x: + version "0.11.5" + resolved "http://registry.npm.taobao.org/ast-types/download/ast-types-0.11.5.tgz#9890825d660c03c28339f315e9fa0a360e31ec28" + +async-each@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/async-each/download/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async-retry@1.2.1: + version "1.2.1" + resolved "http://registry.npm.taobao.org/async-retry/download/async-retry-1.2.1.tgz#308c6c4e1d91e63397a4676290334ae9bda7bcb1" + dependencies: + retry "0.10.1" + +async@2.6.1, async@^2.1.1, async@^2.4.1, async@^2.5.0: + version "2.6.1" + resolved "http://registry.npm.taobao.org/async/download/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + dependencies: + lodash "^4.17.10" + +asynckit@^0.4.0: + version "0.4.0" + resolved "http://registry.npm.taobao.org/asynckit/download/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +atob@^2.1.1: + version "2.1.2" + resolved "http://registry.npm.taobao.org/atob/download/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + +autod-egg@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/autod-egg/download/autod-egg-1.1.0.tgz#bc37bb954661d88ef07e30dc83e11262bf9e30ac" + +autod@^3.0.1: + version "3.0.1" + resolved "http://registry.npm.taobao.org/autod/download/autod-3.0.1.tgz#8478c77cb7d6ee8c4f3eead431454eb29da37eb6" + dependencies: + babel-core "^6.26.0" + babel-preset-env "^1.6.1" + babel-preset-react "^6.24.1" + babel-preset-stage-0 "^6.24.1" + co "^4.6.0" + colors "^1.1.2" + commander "^2.11.0" + crequire "^1.8.1" + debug "^3.1.0" + fs-readdir-recursive "^1.1.0" + glob "^7.1.2" + minimatch "^3.0.4" + printable "^0.0.3" + urllib "^2.25.1" + +await-event@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/await-event/download/await-event-2.1.0.tgz#78e9f92684bae4022f9fa0b5f314a11550f9aa76" + +await-first@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/await-first/download/await-first-1.0.0.tgz#06afa6db7cebe412be9be54e82dd8c6cb4cdb241" + dependencies: + ee-first "^1.1.1" + +axobject-query@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/axobject-query/download/axobject-query-2.0.1.tgz#05dfa705ada8ad9db993fa6896f22d395b0b0a07" + dependencies: + ast-types-flow "0.0.7" + +babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-code-frame/download/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^6.26.0: + version "6.26.3" + resolved "http://registry.npm.taobao.org/babel-core/download/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.1" + debug "^2.6.9" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.8" + slash "^1.0.0" + source-map "^0.5.7" + +babel-eslint@^8.1.2: + version "8.2.6" + resolved "http://registry.npm.taobao.org/babel-eslint/download/babel-eslint-8.2.6.tgz#6270d0c73205628067c0f7ae1693a9e797acefd9" + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/traverse" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + eslint-scope "3.7.1" + eslint-visitor-keys "^1.0.0" + +babel-generator@^6.26.0: + version "6.26.1" + resolved "http://registry.npm.taobao.org/babel-generator/download/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-helper-bindify-decorators@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-bindify-decorators/download/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-builder-binary-assignment-operator-visitor/download/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-builder-react-jsx@^6.24.1: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-helper-builder-react-jsx/download/babel-helper-builder-react-jsx-6.26.0.tgz#39ff8313b75c8b65dceff1f31d383e0ff2a408a0" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + esutils "^2.0.2" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-call-delegate/download/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-helper-define-map/download/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-explode-assignable-expression/download/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-explode-class@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-explode-class/download/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb" + dependencies: + babel-helper-bindify-decorators "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-function-name/download/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-get-function-arity/download/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-hoist-variables/download/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-optimise-call-expression/download/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-helper-regex/download/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-remap-async-to-generator/download/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helper-replace-supers/download/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-helpers/download/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "http://registry.npm.taobao.org/babel-messages/download/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-check-es2015-constants/download/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-async-functions/download/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-async-generators@^6.5.0: + version "6.13.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-async-generators/download/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" + +babel-plugin-syntax-class-constructor-call@^6.18.0: + version "6.18.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-class-constructor-call/download/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416" + +babel-plugin-syntax-class-properties@^6.8.0: + version "6.13.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-class-properties/download/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" + +babel-plugin-syntax-decorators@^6.13.0: + version "6.13.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-decorators/download/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" + +babel-plugin-syntax-do-expressions@^6.8.0: + version "6.13.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-do-expressions/download/babel-plugin-syntax-do-expressions-6.13.0.tgz#5747756139aa26d390d09410b03744ba07e4796d" + +babel-plugin-syntax-dynamic-import@^6.18.0: + version "6.18.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-dynamic-import/download/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-exponentiation-operator/download/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-export-extensions@^6.8.0: + version "6.13.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-export-extensions/download/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721" + +babel-plugin-syntax-flow@^6.18.0: + version "6.18.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-flow/download/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" + +babel-plugin-syntax-function-bind@^6.8.0: + version "6.13.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-function-bind/download/babel-plugin-syntax-function-bind-6.13.0.tgz#48c495f177bdf31a981e732f55adc0bdd2601f46" + +babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0: + version "6.18.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-jsx/download/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + +babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-object-rest-spread/download/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + +babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-syntax-trailing-function-commas/download/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-generator-functions@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-async-generator-functions/download/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-generators "^6.5.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-to-generator@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-async-to-generator/download/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-class-constructor-call@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-class-constructor-call/download/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9" + dependencies: + babel-plugin-syntax-class-constructor-call "^6.18.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-class-properties@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-class-properties/download/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" + dependencies: + babel-helper-function-name "^6.24.1" + babel-plugin-syntax-class-properties "^6.8.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-decorators@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-decorators/download/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d" + dependencies: + babel-helper-explode-class "^6.24.1" + babel-plugin-syntax-decorators "^6.13.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-do-expressions@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-do-expressions/download/babel-plugin-transform-do-expressions-6.22.0.tgz#28ccaf92812d949c2cd1281f690c8fdc468ae9bb" + dependencies: + babel-plugin-syntax-do-expressions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-arrow-functions/download/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-block-scoped-functions/download/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.23.0: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-block-scoping/download/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" + dependencies: + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-plugin-transform-es2015-classes@^6.23.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-classes/download/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.22.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-computed-properties/download/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.23.0: + version "6.23.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-destructuring/download/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.22.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-duplicate-keys/download/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.23.0: + version "6.23.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-for-of/download/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.22.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-function-name/download/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-literals/download/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-modules-amd/download/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.26.2" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-modules-commonjs/download/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-modules-systemjs/download/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-modules-umd/download/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-object-super@^6.22.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-object-super/download/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.23.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-parameters/download/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.22.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-shorthand-properties/download/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-spread/download/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.22.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-sticky-regex/download/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-template-literals/download/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.23.0: + version "6.23.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-typeof-symbol/download/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.22.0: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-es2015-unicode-regex/download/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0, babel-plugin-transform-exponentiation-operator@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-exponentiation-operator/download/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-export-extensions@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-export-extensions/download/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653" + dependencies: + babel-plugin-syntax-export-extensions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-flow-strip-types@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-flow-strip-types/download/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" + dependencies: + babel-plugin-syntax-flow "^6.18.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-function-bind@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-function-bind/download/babel-plugin-transform-function-bind-6.22.0.tgz#c6fb8e96ac296a310b8cf8ea401462407ddf6a97" + dependencies: + babel-plugin-syntax-function-bind "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-object-rest-spread@^6.22.0: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-object-rest-spread/download/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + +babel-plugin-transform-react-display-name@^6.23.0: + version "6.25.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-react-display-name/download/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-self@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-react-jsx-self/download/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-source@^6.22.0: + version "6.22.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-react-jsx-source/download/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-react-jsx/download/babel-plugin-transform-react-jsx-6.24.1.tgz#840a028e7df460dfc3a2d29f0c0d91f6376e66a3" + dependencies: + babel-helper-builder-react-jsx "^6.24.1" + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-regenerator@^6.22.0: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-regenerator/download/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" + dependencies: + regenerator-transform "^0.10.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-plugin-transform-strict-mode/download/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-preset-env@1.7.0, babel-preset-env@^1.6.1: + version "1.7.0" + resolved "http://registry.npm.taobao.org/babel-preset-env/download/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a" + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.23.0" + babel-plugin-transform-es2015-classes "^6.23.0" + babel-plugin-transform-es2015-computed-properties "^6.22.0" + babel-plugin-transform-es2015-destructuring "^6.23.0" + babel-plugin-transform-es2015-duplicate-keys "^6.22.0" + babel-plugin-transform-es2015-for-of "^6.23.0" + babel-plugin-transform-es2015-function-name "^6.22.0" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.23.0" + babel-plugin-transform-es2015-modules-systemjs "^6.23.0" + babel-plugin-transform-es2015-modules-umd "^6.23.0" + babel-plugin-transform-es2015-object-super "^6.22.0" + babel-plugin-transform-es2015-parameters "^6.23.0" + babel-plugin-transform-es2015-shorthand-properties "^6.22.0" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.22.0" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.23.0" + babel-plugin-transform-es2015-unicode-regex "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-regenerator "^6.22.0" + browserslist "^3.2.6" + invariant "^2.2.2" + semver "^5.3.0" + +babel-preset-flow@^6.23.0: + version "6.23.0" + resolved "http://registry.npm.taobao.org/babel-preset-flow/download/babel-preset-flow-6.23.0.tgz#e71218887085ae9a24b5be4169affb599816c49d" + dependencies: + babel-plugin-transform-flow-strip-types "^6.22.0" + +babel-preset-react@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-preset-react/download/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380" + dependencies: + babel-plugin-syntax-jsx "^6.3.13" + babel-plugin-transform-react-display-name "^6.23.0" + babel-plugin-transform-react-jsx "^6.24.1" + babel-plugin-transform-react-jsx-self "^6.22.0" + babel-plugin-transform-react-jsx-source "^6.22.0" + babel-preset-flow "^6.23.0" + +babel-preset-stage-0@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-preset-stage-0/download/babel-preset-stage-0-6.24.1.tgz#5642d15042f91384d7e5af8bc88b1db95b039e6a" + dependencies: + babel-plugin-transform-do-expressions "^6.22.0" + babel-plugin-transform-function-bind "^6.22.0" + babel-preset-stage-1 "^6.24.1" + +babel-preset-stage-1@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-preset-stage-1/download/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0" + dependencies: + babel-plugin-transform-class-constructor-call "^6.24.1" + babel-plugin-transform-export-extensions "^6.22.0" + babel-preset-stage-2 "^6.24.1" + +babel-preset-stage-2@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-preset-stage-2/download/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1" + dependencies: + babel-plugin-syntax-dynamic-import "^6.18.0" + babel-plugin-transform-class-properties "^6.24.1" + babel-plugin-transform-decorators "^6.24.1" + babel-preset-stage-3 "^6.24.1" + +babel-preset-stage-3@^6.24.1: + version "6.24.1" + resolved "http://registry.npm.taobao.org/babel-preset-stage-3/download/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395" + dependencies: + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-generator-functions "^6.24.1" + babel-plugin-transform-async-to-generator "^6.24.1" + babel-plugin-transform-exponentiation-operator "^6.24.1" + babel-plugin-transform-object-rest-spread "^6.22.0" + +babel-register@6.26.0, babel-register@^6.26.0: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-register/download/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-runtime/download/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-template/download/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.24.1, babel-traverse@^6.26.0: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-traverse/download/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "http://registry.npm.taobao.org/babel-types/download/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@7.0.0-beta.44: + version "7.0.0-beta.44" + resolved "http://registry.npm.taobao.org/babylon/download/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d" + +babylon@^6.18.0: + version "6.18.0" + resolved "http://registry.npm.taobao.org/babylon/download/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/balanced-match/download/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@^1.0.2: + version "1.3.0" + resolved "http://registry.npm.taobao.org/base64-js/download/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" + +base@^0.11.1: + version "0.11.2" + resolved "http://registry.npm.taobao.org/base/download/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +bcryptjs@^2.4.3: + version "2.4.3" + resolved "http://registry.npm.taobao.org/bcryptjs/download/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + +before-after-hook@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/before-after-hook/download/before-after-hook-1.1.0.tgz#83165e15a59460d13702cb8febd6a1807896db5a" + +binary-extensions@^1.0.0: + version "1.12.0" + resolved "http://registry.npm.taobao.org/binary-extensions/download/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" + +black-hole-stream@~0.0.1: + version "0.0.1" + resolved "http://registry.npm.taobao.org/black-hole-stream/download/black-hole-stream-0.0.1.tgz#33b7a06b9f1e7453d6041b82974481d2152aea42" + +bluebird@3.5.1: + version "3.5.1" + resolved "http://registry.npm.taobao.org/bluebird/download/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + +bluebird@^3.1.1, bluebird@^3.3.4, bluebird@^3.5.0: + version "3.5.2" + resolved "http://registry.npm.taobao.org/bluebird/download/bluebird-3.5.2.tgz#1be0908e054a751754549c270489c1505d4ab15a" + +blueimp-md5@^2.3.0: + version "2.10.0" + resolved "http://registry.npm.taobao.org/blueimp-md5/download/blueimp-md5-2.10.0.tgz#02f0843921f90dca14f5b8920a38593201d6964d" + +bowser@^1.6.0: + version "1.9.4" + resolved "http://registry.npm.taobao.org/bowser/download/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" + +boxen@^1.2.1: + version "1.3.0" + resolved "http://registry.npm.taobao.org/boxen/download/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" + dependencies: + ansi-align "^2.0.0" + camelcase "^4.0.0" + chalk "^2.0.1" + cli-boxes "^1.0.0" + string-width "^2.0.0" + term-size "^1.2.0" + widest-line "^2.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "http://registry.npm.taobao.org/brace-expansion/download/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.0, braces@^2.3.1: + version "2.3.2" + resolved "http://registry.npm.taobao.org/braces/download/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "http://registry.npm.taobao.org/browser-stdout/download/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + +browserslist@^3.2.6: + version "3.2.8" + resolved "http://registry.npm.taobao.org/browserslist/download/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6" + dependencies: + caniuse-lite "^1.0.30000844" + electron-to-chromium "^1.3.47" + +bson@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/bson/download/bson-1.1.0.tgz#bee57d1fb6a87713471af4e32bcae36de814b5b0" + +bson@~1.0.4, bson@~1.0.5: + version "1.0.9" + resolved "http://registry.npm.taobao.org/bson/download/bson-1.0.9.tgz#12319f8323b1254739b7c6bef8d3e89ae05a2f57" + +btoa-lite@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/btoa-lite/download/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "http://registry.npm.taobao.org/buffer-crc32/download/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/buffer-equal-constant-time/download/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + +buffer-from@^1.0.0, buffer-from@^1.1.0: + version "1.1.1" + resolved "http://registry.npm.taobao.org/buffer-from/download/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + +buffer@^5.1.0: + version "5.2.1" + resolved "http://registry.npm.taobao.org/buffer/download/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "http://registry.npm.taobao.org/builtin-modules/download/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/builtin-status-codes/download/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +bump-file@1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/bump-file/download/bump-file-1.0.0.tgz#892880ef8af84c8df8d94cc2829ef18811503247" + dependencies: + detect-indent "5.0.0" + semver "5.4.1" + +busboy@^0.2.8: + version "0.2.14" + resolved "http://registry.npm.taobao.org/busboy/download/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + +byte@^1.4.0: + version "1.4.1" + resolved "http://registry.npm.taobao.org/byte/download/byte-1.4.1.tgz#a80553d2aae53b1856ab54fa7743e03a20dcc944" + dependencies: + debug "^2.6.6" + long "^3.2.0" + utility "^1.12.0" + +bytes@3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/bytes/download/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + +bytes@~2.2.0: + version "2.2.0" + resolved "http://registry.npm.taobao.org/bytes/download/bytes-2.2.0.tgz#fd35464a403f6f9117c2de3609ecff9cae000588" + +cache-base@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/cache-base/download/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +cache-content-type@^1.0.0, cache-content-type@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/cache-content-type/download/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" + dependencies: + mime-types "^2.1.18" + ylru "^1.2.0" + +cacheable-request@^2.1.1: + version "2.1.4" + resolved "http://registry.npm.taobao.org/cacheable-request/download/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" + dependencies: + clone-response "1.0.2" + get-stream "3.0.0" + http-cache-semantics "3.8.1" + keyv "3.0.0" + lowercase-keys "1.0.0" + normalize-url "2.0.1" + responselike "1.0.2" + +caching-transform@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/caching-transform/download/caching-transform-2.0.0.tgz#e1292bd92d35b6e8b1ed7075726724b3bd64eea0" + dependencies: + make-dir "^1.0.0" + md5-hex "^2.0.0" + package-hash "^2.0.0" + write-file-atomic "^2.0.0" + +call-matcher@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/call-matcher/download/call-matcher-1.1.0.tgz#23b2c1bc7a8394c8be28609d77ddbd5786680432" + dependencies: + core-js "^2.0.0" + deep-equal "^1.0.0" + espurify "^1.6.0" + estraverse "^4.0.0" + +call-me-maybe@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/call-me-maybe/download/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" + +call-signature@0.0.2: + version "0.0.2" + resolved "http://registry.npm.taobao.org/call-signature/download/call-signature-0.0.2.tgz#a84abc825a55ef4cb2b028bd74e205a65b9a4996" + +caller-path@^0.1.0: + version "0.1.0" + resolved "http://registry.npm.taobao.org/caller-path/download/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "http://registry.npm.taobao.org/callsites/download/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +camel-case@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/camel-case/download/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/camelcase-keys/download/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase-keys@^4.0.0: + version "4.2.0" + resolved "http://registry.npm.taobao.org/camelcase-keys/download/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" + dependencies: + camelcase "^4.1.0" + map-obj "^2.0.0" + quick-lru "^1.0.0" + +camelcase@^2.0.0, camelcase@^2.0.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/camelcase/download/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/camelcase/download/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +camelcase@^4.0.0, camelcase@^4.1.0: + version "4.1.0" + resolved "http://registry.npm.taobao.org/camelcase/download/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +camelcase@^5.0.0: + version "5.0.0" + resolved "http://registry.npm.taobao.org/camelcase/download/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" + +caniuse-lite@^1.0.30000844: + version "1.0.30000885" + resolved "http://registry.npm.taobao.org/caniuse-lite/download/caniuse-lite-1.0.30000885.tgz#e889e9f8e7e50e769f2a49634c932b8aee622984" + +capture-stack-trace@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/capture-stack-trace/download/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" + +cfork@^1.6.1, cfork@^1.7.1: + version "1.7.1" + resolved "http://registry.npm.taobao.org/cfork/download/cfork-1.7.1.tgz#c24e9dbb70a728881e0ed6647bf91a7ae6b75525" + dependencies: + utility "^1.12.0" + +chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.1: + version "2.4.1" + resolved "http://registry.npm.taobao.org/chalk/download/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^1.1.3: + version "1.1.3" + resolved "http://registry.npm.taobao.org/chalk/download/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chan@^0.6.1: + version "0.6.1" + resolved "http://registry.npm.taobao.org/chan/download/chan-0.6.1.tgz#ec0ad132e5bc62c27ef10ccbfc4d8dcd8ca00640" + +change-case@^3.0.1: + version "3.0.2" + resolved "http://registry.npm.taobao.org/change-case/download/change-case-3.0.2.tgz#fd48746cce02f03f0a672577d1d3a8dc2eceb037" + dependencies: + camel-case "^3.0.0" + constant-case "^2.0.0" + dot-case "^2.1.0" + header-case "^1.0.0" + is-lower-case "^1.1.0" + is-upper-case "^1.1.0" + lower-case "^1.1.1" + lower-case-first "^1.0.0" + no-case "^2.3.2" + param-case "^2.1.0" + pascal-case "^2.0.0" + path-case "^2.1.0" + sentence-case "^2.1.0" + snake-case "^2.1.0" + swap-case "^1.1.0" + title-case "^2.1.0" + upper-case "^1.1.1" + upper-case-first "^1.1.0" + +chardet@^0.4.0: + version "0.4.2" + resolved "http://registry.npm.taobao.org/chardet/download/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" + +chardet@^0.7.0: + version "0.7.0" + resolved "http://registry.npm.taobao.org/chardet/download/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + +charenc@~0.0.1: + version "0.0.2" + resolved "http://registry.npm.taobao.org/charenc/download/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + +chokidar@^2.0.0: + version "2.0.4" + resolved "http://registry.npm.taobao.org/chokidar/download/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" + dependencies: + anymatch "^2.0.0" + async-each "^1.0.0" + braces "^2.3.0" + glob-parent "^3.1.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + lodash.debounce "^4.0.8" + normalize-path "^2.1.1" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + upath "^1.0.5" + optionalDependencies: + fsevents "^1.2.2" + +chownr@^1.0.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/chownr/download/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" + +ci-info@^1.3.0, ci-info@^1.5.0: + version "1.5.1" + resolved "http://registry.npm.taobao.org/ci-info/download/ci-info-1.5.1.tgz#17e8eb5de6f8b2b6038f0cbb714d410bfa9f3030" + +circular-json@^0.3.1: + version "0.3.3" + resolved "http://registry.npm.taobao.org/circular-json/download/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +circular-json@^0.5.4, circular-json@^0.5.5: + version "0.5.7" + resolved "http://registry.npm.taobao.org/circular-json/download/circular-json-0.5.7.tgz#b8be478d72ea58c7eeda26bf1cf1fba43d188842" + +class-utils@^0.3.5: + version "0.3.6" + resolved "http://registry.npm.taobao.org/class-utils/download/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cli-boxes@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/cli-boxes/download/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/cli-cursor/download/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + +cli-spinners@^1.1.0: + version "1.3.1" + resolved "http://registry.npm.taobao.org/cli-spinners/download/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a" + +cli-width@^2.0.0: + version "2.2.0" + resolved "http://registry.npm.taobao.org/cli-width/download/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + +cliui@^3.0.3, cliui@^3.2.0: + version "3.2.0" + resolved "http://registry.npm.taobao.org/cliui/download/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +cliui@^4.0.0: + version "4.1.0" + resolved "http://registry.npm.taobao.org/cliui/download/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +clone-response@1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/clone-response/download/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + dependencies: + mimic-response "^1.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "http://registry.npm.taobao.org/clone/download/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + +cluster-client@^2.1.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/cluster-client/download/cluster-client-2.1.1.tgz#794fa465e0cd117cb720a222168ead8c6671dbe7" + dependencies: + byte "^1.4.0" + co "^4.6.0" + debug "^3.1.0" + egg-logger "^1.6.1" + is-type-of "^1.2.0" + json-stringify-safe "^5.0.1" + long "^4.0.0" + mz-modules "^2.1.0" + sdk-base "^3.4.0" + serialize-json "^1.0.2" + tcp-base "^3.1.0" + utility "^1.13.1" + +cluster-key-slot@^1.0.6: + version "1.0.12" + resolved "http://registry.npm.taobao.org/cluster-key-slot/download/cluster-key-slot-1.0.12.tgz#d5deff2a520717bc98313979b687309b2d368e29" + +cluster-reload@^1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/cluster-reload/download/cluster-reload-1.0.2.tgz#346bf5849d18e4590bcc1b30ee470d4cabf15c10" + +co-body@^6.0.0: + version "6.0.0" + resolved "http://registry.npm.taobao.org/co-body/download/co-body-6.0.0.tgz#965b9337d7f5655480787471f4237664820827e3" + dependencies: + inflation "^2.0.0" + qs "^6.5.2" + raw-body "^2.3.3" + type-is "^1.6.16" + +co-busboy@^1.4.0: + version "1.4.0" + resolved "http://registry.npm.taobao.org/co-busboy/download/co-busboy-1.4.0.tgz#ac9b85c4a966f03b7df55d53746a0dc9c93fa741" + dependencies: + black-hole-stream "~0.0.1" + busboy "^0.2.8" + chan "^0.6.1" + +co-defer@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/co-defer/download/co-defer-1.0.0.tgz#3e4a787a8eed6b0a21ee287c094f7e8de0d3c818" + +co-gather@^0.0.1: + version "0.0.1" + resolved "http://registry.npm.taobao.org/co-gather/download/co-gather-0.0.1.tgz#efa35fbef02c9f647d8a740b3f5db731862535bc" + dependencies: + co-thread "0.0.1" + +co-mocha@^1.2.2: + version "1.2.2" + resolved "http://registry.npm.taobao.org/co-mocha/download/co-mocha-1.2.2.tgz#c4fdf24d37f43ca4da668b14542a96e9377479ab" + dependencies: + co "^4.0.0" + is-generator "^1.0.1" + +co-priority-queue@^1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/co-priority-queue/download/co-priority-queue-1.0.3.tgz#b9646e00e84439fb15563d61b68bee2ba5169b4f" + +co-thread@0.0.1: + version "0.0.1" + resolved "http://registry.npm.taobao.org/co-thread/download/co-thread-0.0.1.tgz#57713f0ef4b87e5595d4f23711ffe4b3b6de5e74" + +co@^4.0.0, co@^4.6.0: + version "4.6.0" + resolved "http://registry.npm.taobao.org/co/download/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/code-point-at/download/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +coffee@^5.1.0: + version "5.1.0" + resolved "http://registry.npm.taobao.org/coffee/download/coffee-5.1.0.tgz#a332a8f590f58321f3106d83b3e92e68bb860ddc" + dependencies: + cross-spawn "^6.0.5" + debug "^3.1.0" + is-type-of "^1.2.0" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/collection-visit/download/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "http://registry.npm.taobao.org/color-convert/download/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "http://registry.npm.taobao.org/color-name/download/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +colors@^1.1.2: + version "1.3.2" + resolved "http://registry.npm.taobao.org/colors/download/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b" + +combined-stream@1.0.6: + version "1.0.6" + resolved "http://registry.npm.taobao.org/combined-stream/download/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + +commander@2.15.1: + version "2.15.1" + resolved "http://registry.npm.taobao.org/commander/download/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + +commander@^2.11.0, commander@^2.9.0: + version "2.18.0" + resolved "http://registry.npm.taobao.org/commander/download/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" + +commander@~2.17.1: + version "2.17.1" + resolved "http://registry.npm.taobao.org/commander/download/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + +common-bin@^2.7.1, common-bin@^2.7.3: + version "2.7.3" + resolved "http://registry.npm.taobao.org/common-bin/download/common-bin-2.7.3.tgz#9841b72d954ad0d62f08b0f530fde56aff0154e2" + dependencies: + chalk "^2.1.0" + change-case "^3.0.1" + co "^4.6.0" + dargs "^5.1.0" + debug "^3.0.1" + is-type-of "^1.2.0" + semver "^5.4.1" + yargs "^8.0.2" + yargs-parser "^7.0.0" + +commondir@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/commondir/download/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +compare-func@^1.3.1: + version "1.3.2" + resolved "http://registry.npm.taobao.org/compare-func/download/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648" + dependencies: + array-ify "^1.0.0" + dot-prop "^3.0.0" + +component-emitter@^1.2.0, component-emitter@^1.2.1: + version "1.2.1" + resolved "http://registry.npm.taobao.org/component-emitter/download/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + +compressible@^2.0.6: + version "2.0.15" + resolved "http://registry.npm.taobao.org/compressible/download/compressible-2.0.15.tgz#857a9ab0a7e5a07d8d837ed43fe2defff64fe212" + dependencies: + mime-db ">= 1.36.0 < 2" + +concat-map@0.0.1: + version "0.0.1" + resolved "http://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.6.0: + version "1.6.2" + resolved "http://registry.npm.taobao.org/concat-stream/download/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +configstore@^3.0.0: + version "3.1.2" + resolved "http://registry.npm.taobao.org/configstore/download/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/console-control-strings/download/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +constant-case@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/constant-case/download/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46" + dependencies: + snake-case "^2.1.0" + upper-case "^1.1.1" + +contains-path@^0.1.0: + version "0.1.0" + resolved "http://registry.npm.taobao.org/contains-path/download/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + +content-disposition@~0.5.2: + version "0.5.2" + resolved "http://registry.npm.taobao.org/content-disposition/download/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@^1.0.2, content-type@^1.0.4: + version "1.0.4" + resolved "http://registry.npm.taobao.org/content-type/download/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + +conventional-changelog-angular@^1.6.6: + version "1.6.6" + resolved "http://registry.npm.taobao.org/conventional-changelog-angular/download/conventional-changelog-angular-1.6.6.tgz#b27f2b315c16d0a1f23eb181309d0e6a4698ea0f" + dependencies: + compare-func "^1.3.1" + q "^1.5.1" + +conventional-changelog-atom@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/conventional-changelog-atom/download/conventional-changelog-atom-2.0.0.tgz#cd6453469cfb8fc345af3391b92990251c95558b" + dependencies: + q "^1.5.1" + +conventional-changelog-codemirror@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/conventional-changelog-codemirror/download/conventional-changelog-codemirror-2.0.0.tgz#bfb61ccabacdd3bf8425a5cbe92276c86c5a0c1e" + dependencies: + q "^1.5.1" + +conventional-changelog-core@^3.1.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/conventional-changelog-core/download/conventional-changelog-core-3.1.0.tgz#96a81bb3301b4b2a3dc2851cc54c5fb674ac1942" + dependencies: + conventional-changelog-writer "^4.0.0" + conventional-commits-parser "^3.0.0" + dateformat "^3.0.0" + get-pkg-repo "^1.0.0" + git-raw-commits "^2.0.0" + git-remote-origin-url "^2.0.0" + git-semver-tags "^2.0.0" + lodash "^4.2.1" + normalize-package-data "^2.3.5" + q "^1.5.1" + read-pkg "^1.1.0" + read-pkg-up "^1.0.1" + through2 "^2.0.0" + +conventional-changelog-ember@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/conventional-changelog-ember/download/conventional-changelog-ember-2.0.1.tgz#5a5595b9ed50a6daca4bd3508a47ffe4a1a7152f" + dependencies: + q "^1.5.1" + +conventional-changelog-eslint@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/conventional-changelog-eslint/download/conventional-changelog-eslint-3.0.0.tgz#cc5376cb29a622c1ade197e155bf054640c05cd3" + dependencies: + q "^1.5.1" + +conventional-changelog-express@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/conventional-changelog-express/download/conventional-changelog-express-2.0.0.tgz#d3d020118fbfce21a75e025ec097101e355a2361" + dependencies: + q "^1.5.1" + +conventional-changelog-jquery@^0.1.0: + version "0.1.0" + resolved "http://registry.npm.taobao.org/conventional-changelog-jquery/download/conventional-changelog-jquery-0.1.0.tgz#0208397162e3846986e71273b6c79c5b5f80f510" + dependencies: + q "^1.4.1" + +conventional-changelog-jscs@^0.1.0: + version "0.1.0" + resolved "http://registry.npm.taobao.org/conventional-changelog-jscs/download/conventional-changelog-jscs-0.1.0.tgz#0479eb443cc7d72c58bf0bcf0ef1d444a92f0e5c" + dependencies: + q "^1.4.1" + +conventional-changelog-jshint@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/conventional-changelog-jshint/download/conventional-changelog-jshint-2.0.0.tgz#7a038330f485082e489f47f5d07539036949f87d" + dependencies: + compare-func "^1.3.1" + q "^1.5.1" + +conventional-changelog-preset-loader@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/conventional-changelog-preset-loader/download/conventional-changelog-preset-loader-2.0.1.tgz#d134734e0cc1b91b88b30586c5991f31442029f1" + +conventional-changelog-writer@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/conventional-changelog-writer/download/conventional-changelog-writer-4.0.0.tgz#3ed983c8ef6a3aa51fe44e82c9c75e86f1b5aa42" + dependencies: + compare-func "^1.3.1" + conventional-commits-filter "^2.0.0" + dateformat "^3.0.0" + handlebars "^4.0.2" + json-stringify-safe "^5.0.1" + lodash "^4.2.1" + meow "^4.0.0" + semver "^5.5.0" + split "^1.0.0" + through2 "^2.0.0" + +conventional-changelog@2.0.3: + version "2.0.3" + resolved "http://registry.npm.taobao.org/conventional-changelog/download/conventional-changelog-2.0.3.tgz#779cff582c0091d2b24574003eaa82ef5ddf653d" + dependencies: + conventional-changelog-angular "^1.6.6" + conventional-changelog-atom "^2.0.0" + conventional-changelog-codemirror "^2.0.0" + conventional-changelog-core "^3.1.0" + conventional-changelog-ember "^2.0.1" + conventional-changelog-eslint "^3.0.0" + conventional-changelog-express "^2.0.0" + conventional-changelog-jquery "^0.1.0" + conventional-changelog-jscs "^0.1.0" + conventional-changelog-jshint "^2.0.0" + conventional-changelog-preset-loader "^2.0.1" + +conventional-commits-filter@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/conventional-commits-filter/download/conventional-commits-filter-2.0.0.tgz#a0ce1d1ff7a1dd7fab36bee8e8256d348d135651" + dependencies: + is-subset "^0.1.1" + modify-values "^1.0.0" + +conventional-commits-parser@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/conventional-commits-parser/download/conventional-commits-parser-3.0.0.tgz#7f604549a50bd8f60443fbe515484b1c2f06a5c4" + dependencies: + JSONStream "^1.0.4" + is-text-path "^1.0.0" + lodash "^4.2.1" + meow "^4.0.0" + split2 "^2.0.0" + through2 "^2.0.0" + trim-off-newlines "^1.0.0" + +conventional-recommended-bump@4.0.1: + version "4.0.1" + resolved "http://registry.npm.taobao.org/conventional-recommended-bump/download/conventional-recommended-bump-4.0.1.tgz#304a45a412cfec050a10ea2e7e4a89320eaf3991" + dependencies: + concat-stream "^1.6.0" + conventional-changelog-preset-loader "^2.0.1" + conventional-commits-filter "^2.0.0" + conventional-commits-parser "^3.0.0" + git-raw-commits "^2.0.0" + git-semver-tags "^2.0.0" + meow "^4.0.0" + q "^1.5.1" + +convert-source-map@^1.1.0, convert-source-map@^1.1.1, convert-source-map@^1.5.1: + version "1.6.0" + resolved "http://registry.npm.taobao.org/convert-source-map/download/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + dependencies: + safe-buffer "~5.1.1" + +cookie@0.3.1, cookie@^0.3.1: + version "0.3.1" + resolved "http://registry.npm.taobao.org/cookie/download/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +cookiejar@^2.1.0: + version "2.1.2" + resolved "http://registry.npm.taobao.org/cookiejar/download/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + +cookies@~0.7.1: + version "0.7.2" + resolved "http://registry.npm.taobao.org/cookies/download/cookies-0.7.2.tgz#52736976126658af7713d7f858f7d21f99dab486" + dependencies: + depd "~1.1.2" + keygrip "~1.0.2" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "http://registry.npm.taobao.org/copy-descriptor/download/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + +copy-to@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/copy-to/download/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5" + +core-js@^2.0.0, core-js@^2.4.0, core-js@^2.5.0: + version "2.5.7" + resolved "http://registry.npm.taobao.org/core-js/download/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" + +core-util-is@^1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "http://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cp-file@^6.0.0: + version "6.0.0" + resolved "http://registry.npm.taobao.org/cp-file/download/cp-file-6.0.0.tgz#f38477ece100b403fcf780fd34d030486beb693e" + dependencies: + graceful-fs "^4.1.2" + make-dir "^1.0.0" + nested-error-stacks "^2.0.0" + pify "^3.0.0" + safe-buffer "^5.0.1" + +cpy@7.0.1: + version "7.0.1" + resolved "http://registry.npm.taobao.org/cpy/download/cpy-7.0.1.tgz#d817e4d81bd7f0f25ff812796c5f1392dc0fb485" + dependencies: + arrify "^1.0.1" + cp-file "^6.0.0" + globby "^8.0.1" + nested-error-stacks "^2.0.0" + +crc@^3.4.4: + version "3.8.0" + resolved "http://registry.npm.taobao.org/crc/download/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" + dependencies: + buffer "^5.1.0" + +create-error-class@^3.0.0: + version "3.0.2" + resolved "http://registry.npm.taobao.org/create-error-class/download/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + dependencies: + capture-stack-trace "^1.0.0" + +crequire@^1.8.0, crequire@^1.8.1: + version "1.8.1" + resolved "http://registry.npm.taobao.org/crequire/download/crequire-1.8.1.tgz#ac81f204786b5f201194eb1698cf441b10a4b57d" + +cron-parser@^2.4.4: + version "2.6.0" + resolved "http://registry.npm.taobao.org/cron-parser/download/cron-parser-2.6.0.tgz#ae2514ceda9ccb540256e201bdd23ae814e03674" + dependencies: + is-nan "^1.2.1" + moment-timezone "^0.5.0" + +cross-spawn@^4: + version "4.0.2" + resolved "http://registry.npm.taobao.org/cross-spawn/download/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^5.0.1, cross-spawn@^5.1.0: + version "5.1.0" + resolved "http://registry.npm.taobao.org/cross-spawn/download/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^6.0.5: + version "6.0.5" + resolved "http://registry.npm.taobao.org/cross-spawn/download/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +crypt@~0.0.1: + version "0.0.2" + resolved "http://registry.npm.taobao.org/crypt/download/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/crypto-random-string/download/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + +csrf@^3.0.6: + version "3.0.6" + resolved "http://registry.npm.taobao.org/csrf/download/csrf-3.0.6.tgz#b61120ddceeafc91e76ed5313bb5c0b2667b710a" + dependencies: + rndm "1.2.0" + tsscmp "1.0.5" + uid-safe "2.1.4" + +cssfilter@0.0.10: + version "0.0.10" + resolved "http://registry.npm.taobao.org/cssfilter/download/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "http://registry.npm.taobao.org/currently-unhandled/download/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +d@1: + version "1.0.0" + resolved "http://registry.npm.taobao.org/d/download/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + +damerau-levenshtein@^1.0.4: + version "1.0.4" + resolved "http://registry.npm.taobao.org/damerau-levenshtein/download/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514" + +dargs@^4.0.1: + version "4.1.0" + resolved "http://registry.npm.taobao.org/dargs/download/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17" + dependencies: + number-is-nan "^1.0.0" + +dargs@^5.1.0: + version "5.1.0" + resolved "http://registry.npm.taobao.org/dargs/download/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829" + +data-uri-to-buffer@1: + version "1.2.0" + resolved "http://registry.npm.taobao.org/data-uri-to-buffer/download/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" + +dateformat@^2.0.0: + version "2.2.0" + resolved "http://registry.npm.taobao.org/dateformat/download/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" + +dateformat@^3.0.0: + version "3.0.3" + resolved "http://registry.npm.taobao.org/dateformat/download/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" + +debounce@^1.1.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/debounce/download/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" + +debug-log@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/debug-log/download/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f" + +debug@2, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.1, debug@^2.6.2, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: + version "2.6.9" + resolved "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@3.1.0, debug@~3.1.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/debug/download/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +debug@^3.0.1, debug@^3.1.0: + version "3.2.5" + resolved "http://registry.npm.taobao.org/debug/download/debug-3.2.5.tgz#c2418fbfd7a29f4d4f70ff4cea604d4b64c46407" + dependencies: + ms "^2.1.1" + +debug@^4.0.1: + version "4.0.1" + resolved "http://registry.npm.taobao.org/debug/download/debug-4.0.1.tgz#f9bb36d439b8d1f0dd52d8fb6b46e4ebb8c1cd5b" + dependencies: + ms "^2.1.1" + +decamelize-keys@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/decamelize-keys/download/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "http://registry.npm.taobao.org/decamelize/download/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "http://registry.npm.taobao.org/decode-uri-component/download/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +decompress-response@^3.3.0: + version "3.3.0" + resolved "http://registry.npm.taobao.org/decompress-response/download/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + dependencies: + mimic-response "^1.0.0" + +deep-equal@^1.0.0, deep-equal@~1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/deep-equal/download/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "http://registry.npm.taobao.org/deep-extend/download/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + +deep-is@~0.1.3: + version "0.1.3" + resolved "http://registry.npm.taobao.org/deep-is/download/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +default-require-extensions@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/default-require-extensions/download/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" + dependencies: + strip-bom "^3.0.0" + +default-user-agent@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/default-user-agent/download/default-user-agent-1.0.0.tgz#16c46efdcaba3edc45f24f2bd4868b01b7c2adc6" + dependencies: + os-name "~1.0.3" + +defaults@^1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/defaults/download/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + dependencies: + clone "^1.0.2" + +define-properties@^1.1.1, define-properties@^1.1.2: + version "1.1.3" + resolved "http://registry.npm.taobao.org/define-properties/download/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "http://registry.npm.taobao.org/define-property/download/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/define-property/download/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "http://registry.npm.taobao.org/define-property/download/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +degenerator@^1.0.4: + version "1.0.4" + resolved "http://registry.npm.taobao.org/degenerator/download/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095" + dependencies: + ast-types "0.x.x" + escodegen "1.x.x" + esprima "3.x.x" + +del@^2.0.2: + version "2.2.2" + resolved "http://registry.npm.taobao.org/del/download/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/delayed-stream/download/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/delegates/download/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +denque@^1.1.0: + version "1.3.0" + resolved "http://registry.npm.taobao.org/denque/download/denque-1.3.0.tgz#681092ef44a630246d3f6edb2a199230eae8e76b" + +depd@^1.1.0, depd@^1.1.2, depd@~1.1.2: + version "1.1.2" + resolved "http://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + +destroy@^1.0.4: + version "1.0.4" + resolved "http://registry.npm.taobao.org/destroy/download/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +detect-indent@5.0.0: + version "5.0.0" + resolved "http://registry.npm.taobao.org/detect-indent/download/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/detect-indent/download/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "http://registry.npm.taobao.org/detect-libc/download/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + +detect-port@^1.2.2, detect-port@^1.2.3: + version "1.2.3" + resolved "http://registry.npm.taobao.org/detect-port/download/detect-port-1.2.3.tgz#15bf49820d02deb84bfee0a74876b32d791bf610" + dependencies: + address "^1.0.1" + debug "^2.6.0" + +dicer@0.2.5: + version "0.2.5" + resolved "http://registry.npm.taobao.org/dicer/download/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + +diff-match-patch@^1.0.0: + version "1.0.4" + resolved "http://registry.npm.taobao.org/diff-match-patch/download/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" + +diff@3.5.0, diff@^3.1.0: + version "3.5.0" + resolved "http://registry.npm.taobao.org/diff/download/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + +digest-header@^0.0.1: + version "0.0.1" + resolved "http://registry.npm.taobao.org/digest-header/download/digest-header-0.0.1.tgz#11ccf6deec5766ac379744d901c12cba49514be6" + dependencies: + utility "0.1.11" + +dir-glob@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/dir-glob/download/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + dependencies: + arrify "^1.0.1" + path-type "^3.0.0" + +doctrine@1.5.0: + version "1.5.0" + resolved "http://registry.npm.taobao.org/doctrine/download/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/doctrine/download/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + dependencies: + esutils "^2.0.2" + +dot-case@^2.1.0: + version "2.1.1" + resolved "http://registry.npm.taobao.org/dot-case/download/dot-case-2.1.1.tgz#34dcf37f50a8e93c2b3bca8bb7fb9155c7da3bee" + dependencies: + no-case "^2.2.0" + +dot-prop@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/dot-prop/download/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" + dependencies: + is-obj "^1.0.0" + +dot-prop@^4.1.0: + version "4.2.0" + resolved "http://registry.npm.taobao.org/dot-prop/download/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + dependencies: + is-obj "^1.0.0" + +duplexer3@^0.1.4: + version "0.1.4" + resolved "http://registry.npm.taobao.org/duplexer3/download/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + +duplexer@^0.1.1, duplexer@~0.1.1: + version "0.1.1" + resolved "http://registry.npm.taobao.org/duplexer/download/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + +duplexify@^3.6.0: + version "3.6.0" + resolved "http://registry.npm.taobao.org/duplexify/download/duplexify-3.6.0.tgz#592903f5d80b38d037220541264d69a198fb3410" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "http://registry.npm.taobao.org/eastasianwidth/download/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "http://registry.npm.taobao.org/ecdsa-sig-formatter/download/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1, ee-first@^1.1.1, ee-first@~1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +egg-bin@^4.3.5: + version "4.9.0" + resolved "http://registry.npm.taobao.org/egg-bin/download/egg-bin-4.9.0.tgz#e6bc89c29bfda6ad7f80681ef92d9653c410692f" + dependencies: + autod "^3.0.1" + chalk "^2.4.1" + co-mocha "^1.2.2" + common-bin "^2.7.3" + debug "^3.1.0" + detect-port "^1.2.3" + egg-utils "^2.4.0" + espower-typescript "^9.0.1" + globby "^8.0.1" + inspector-proxy "^1.2.1" + intelli-espower-loader "^1.0.1" + jest-changed-files "^23.4.2" + mocha "^5.2.0" + mz-modules "^2.1.0" + nyc "^13.0.1" + power-assert "^1.6.0" + semver "^5.5.0" + source-map-support "^0.5.6" + test-exclude "^5.0.0" + ts-node "^7.0.0" + ypkgfiles "^1.6.0" + +egg-ci@^1.8.0: + version "1.8.0" + resolved "http://registry.npm.taobao.org/egg-ci/download/egg-ci-1.8.0.tgz#4f86edfd6cea4436d3266bd26a750c2162b3969d" + dependencies: + nunjucks "^3.0.1" + +egg-cluster@^1.19.1: + version "1.20.0" + resolved "http://registry.npm.taobao.org/egg-cluster/download/egg-cluster-1.20.0.tgz#11519a675d8dc7d602777761a319dd0a9d77e6a0" + dependencies: + await-event "^2.1.0" + cfork "^1.7.1" + cluster-reload "^1.0.2" + co "^4.6.0" + debug "^3.1.0" + depd "^1.1.2" + detect-port "^1.2.2" + egg-logger "^1.6.2" + egg-utils "^2.4.0" + get-ready "^2.0.1" + graceful-process "^1.2.0" + is-type-of "^1.2.0" + mz-modules "^2.1.0" + semver "^5.5.0" + sendmessage "^1.1.0" + utility "^1.13.1" + +egg-console@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/egg-console/download/egg-console-2.0.1.tgz#bf7d04f671f9ea8c20870b1f2d0f9f00d4fb74c6" + dependencies: + colors "^1.1.2" + egg-logger "^1.6.0" + +egg-cookies@^2.2.6: + version "2.2.6" + resolved "http://registry.npm.taobao.org/egg-cookies/download/egg-cookies-2.2.6.tgz#0f68a7ace2017bb4ed2331f8078f35b95ff43f41" + dependencies: + debug "^3.1.0" + scmp "^2.0.0" + utility "^1.14.0" + +egg-core@^4.10.0: + version "4.10.0" + resolved "http://registry.npm.taobao.org/egg-core/download/egg-core-4.10.0.tgz#d221bad8b96eaa6e93dd66cf8ed523dbba1c9c97" + dependencies: + co "^4.6.0" + debug "^3.1.0" + depd "^1.1.2" + egg-logger "^1.7.1" + egg-path-matching "^1.0.1" + extend2 "^1.0.0" + get-ready "^2.0.1" + globby "^8.0.1" + inflection "^1.12.0" + is-type-of "^1.2.0" + koa "^2.5.2" + koa-convert "^1.2.0" + koa-router "^7.4.0" + node-homedir "^1.1.1" + ready-callback "^2.1.0" + utility "^1.14.0" + +egg-cors@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/egg-cors/download/egg-cors-2.1.0.tgz#46bc924f2a33b6ebe5625866cb266384aabaa5d8" + dependencies: + "@koa/cors" "^2.2.1" + +egg-development@^2.4.1: + version "2.4.1" + resolved "http://registry.npm.taobao.org/egg-development/download/egg-development-2.4.1.tgz#098292396919ad0acb4a3d41ac8aea61faac7595" + dependencies: + debounce "^1.1.0" + multimatch "^2.1.0" + mz "^2.7.0" + mz-modules "^2.1.0" + utility "^1.13.1" + +egg-i18n@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/egg-i18n/download/egg-i18n-2.0.0.tgz#2180ea3ebbd7e3bf610e5cc8582e9106b0f69111" + dependencies: + debug "^3.1.0" + koa-locales "^1.7.0" + +egg-jsonp@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/egg-jsonp/download/egg-jsonp-2.0.0.tgz#d0145faa48b5422681883430421e7c245897191f" + dependencies: + is-type-of "^1.2.0" + jsonp-body "^1.0.0" + +egg-logger@^1.6.0, egg-logger@^1.6.1, egg-logger@^1.6.2, egg-logger@^1.7.1: + version "1.7.1" + resolved "http://registry.npm.taobao.org/egg-logger/download/egg-logger-1.7.1.tgz#1536acf185eeaf1d5211241ca5bdcaaabe2ab228" + dependencies: + chalk "^1.1.3" + circular-json "^0.5.4" + debug "^2.6.2" + depd "^1.1.0" + iconv-lite "^0.4.15" + mkdirp "^0.5.1" + utility "^1.11.0" + +egg-logrotator@^3.0.3: + version "3.0.3" + resolved "http://registry.npm.taobao.org/egg-logrotator/download/egg-logrotator-3.0.3.tgz#232b59b110983afd7637da607d0d5c86dcee0b98" + dependencies: + debug "^3.1.0" + moment "^2.19.3" + mz "^2.7.0" + +egg-mock@^3.14.0: + version "3.20.1" + resolved "http://registry.npm.taobao.org/egg-mock/download/egg-mock-3.20.1.tgz#bb59619a5d960232a1446ef7f17c21acea2a07bb" + dependencies: + "@types/power-assert" "^1.5.0" + await-event "^2.1.0" + co "^4.6.0" + coffee "^5.1.0" + debug "^4.0.1" + detect-port "^1.2.3" + egg-logger "^1.7.1" + egg-utils "^2.4.1" + extend2 "^1.0.0" + get-ready "^2.0.1" + globby "^8.0.1" + is-type-of "^1.2.0" + ko-sleep "^1.0.3" + merge-descriptors "^1.0.1" + methods "^1.1.2" + mm "^2.4.1" + power-assert "^1.6.1" + rimraf "^2.6.2" + supertest "^3.3.0" + urllib "^2.29.1" + +egg-mongoose@^3.1.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/egg-mongoose/download/egg-mongoose-3.1.0.tgz#daeca29ff2cc8cc57b3532374e19ad37fefd2c90" + dependencies: + await-first "^1.0.0" + mongoose "^5.0.17" + +egg-multipart@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/egg-multipart/download/egg-multipart-2.1.0.tgz#219c886235da9d76b22bfd9d0aabba47edabe40c" + dependencies: + co-busboy "^1.4.0" + humanize-bytes "^1.0.1" + +egg-onerror@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/egg-onerror/download/egg-onerror-2.1.0.tgz#e05dc08e39aec16518b64053b8bc874110aba9f7" + dependencies: + cookie "^0.3.1" + koa-onerror "^4.0.0" + mustache "^2.3.0" + stack-trace "^0.0.10" + +egg-oss@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/egg-oss/download/egg-oss-1.1.0.tgz#848d778e76619fc69d4d50e9120b4ed55d9e77a1" + dependencies: + ali-oss "^4.10.1" + +egg-path-matching@^1.0.0, egg-path-matching@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/egg-path-matching/download/egg-path-matching-1.0.1.tgz#ccfc4e408acd1cf94a7f672fb8c969e456883913" + dependencies: + path-to-regexp "^1.7.0" + +egg-redis@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/egg-redis/download/egg-redis-2.0.0.tgz#247be54b2a77426b2f96e09c2a5bcc518eb05daf" + dependencies: + ioredis "^3.2.2" + +egg-router-plus@^1.2.2: + version "1.3.0" + resolved "http://registry.npm.taobao.org/egg-router-plus/download/egg-router-plus-1.3.0.tgz#94f73447141e75ce82620d04d4dc8e500ea8b27c" + dependencies: + is-type-of "^1.2.0" + +egg-schedule@^3.4.0: + version "3.4.0" + resolved "http://registry.npm.taobao.org/egg-schedule/download/egg-schedule-3.4.0.tgz#1a70bda4575b37ff86a0c5c7bcdd68557555ed07" + dependencies: + cron-parser "^2.4.4" + humanize-ms "^1.2.1" + is-type-of "^1.2.0" + safe-timers "^1.1.0" + utility "^1.13.1" + +egg-scripts@^2.5.0: + version "2.9.1" + resolved "http://registry.npm.taobao.org/egg-scripts/download/egg-scripts-2.9.1.tgz#117508b4ca5ca2b63e9c0d1cc0c0aba6d02bb388" + dependencies: + common-bin "^2.7.1" + debug "^3.1.0" + egg-utils "^2.3.0" + moment "^2.19.2" + mz "^2.7.0" + mz-modules "^2.0.0" + node-homedir "^1.1.0" + runscript "^1.3.0" + source-map-support "^0.5.4" + zlogger "^1.1.0" + +egg-security@^2.4.0: + version "2.4.0" + resolved "http://registry.npm.taobao.org/egg-security/download/egg-security-2.4.0.tgz#8bab9ac82069477b08145744d4ff29d552776df8" + dependencies: + csrf "^3.0.6" + debug "^3.1.0" + delegates "^1.0.0" + egg-path-matching "^1.0.0" + escape-html "^1.0.3" + extend "^3.0.1" + ip "^1.1.5" + koa-compose "^4.0.0" + matcher "^1.1.1" + methods "^1.1.2" + nanoid "^1.1.1" + platform "^1.3.4" + statuses "^1.5.0" + type-is "^1.6.15" + xss "^0.3.4" + +egg-sentry@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/egg-sentry/download/egg-sentry-1.0.0.tgz#683cb54f9b244d81511d6cd23760f1633c65517b" + dependencies: + raven "^2.2.1" + +egg-session@^3.1.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/egg-session/download/egg-session-3.1.0.tgz#5b3baf0f6072fa55b5d13be2e6cbba17e2553bfe" + dependencies: + koa-session "^5.7.0" + +egg-static@^2.1.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/egg-static/download/egg-static-2.1.1.tgz#d76ce75162bd27a002e4922a73c868be9e412496" + dependencies: + koa-compose "^4.0.0" + koa-range "^0.3.0" + koa-static-cache "^5.1.1" + mkdirp "^0.5.1" + ylru "^1.2.0" + +egg-utils@^2.3.0, egg-utils@^2.4.0, egg-utils@^2.4.1: + version "2.4.1" + resolved "http://registry.npm.taobao.org/egg-utils/download/egg-utils-2.4.1.tgz#bc7a330315a100bc45838a54f744fdd5d47c332a" + dependencies: + mkdirp "^0.5.1" + utility "^1.13.1" + +egg-validate@^1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/egg-validate/download/egg-validate-1.1.1.tgz#38286a8c8b8c9dbe3ccca237ec75568a7d1e52ee" + dependencies: + parameter "^2.4.0" + +egg-view@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/egg-view/download/egg-view-2.1.0.tgz#aa7ca0344213f5d72147d6001bc0503f858b425c" + dependencies: + mz "^2.7.0" + +egg-watcher@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/egg-watcher/download/egg-watcher-3.0.0.tgz#0f099f444c7a557d2eb0c0f625110204faff7e65" + dependencies: + camelcase "^4.1.0" + sdk-base "^3.3.0" + wt "^1.1.1" + +egg@^2.2.1: + version "2.11.2" + resolved "http://registry.npm.taobao.org/egg/download/egg-2.11.2.tgz#f360a530a7aa484ea9b395071b8c1d7e4fdddad6" + dependencies: + "@types/accepts" "^1.3.5" + "@types/koa" "^2.0.46" + "@types/koa-router" "^7.0.31" + "@types/urllib" "^2.28.0" + accepts "^1.3.5" + agentkeepalive "^3.5.1" + cache-content-type "^1.0.1" + circular-json "^0.5.5" + cluster-client "^2.1.1" + debug "^4.0.1" + delegates "^1.0.0" + egg-cluster "^1.19.1" + egg-cookies "^2.2.6" + egg-core "^4.10.0" + egg-development "^2.4.1" + egg-i18n "^2.0.0" + egg-jsonp "^2.0.0" + egg-logger "^1.7.1" + egg-logrotator "^3.0.3" + egg-multipart "^2.1.0" + egg-onerror "^2.1.0" + egg-schedule "^3.4.0" + egg-security "^2.4.0" + egg-session "^3.1.0" + egg-static "^2.1.1" + egg-view "^2.1.0" + egg-watcher "^3.0.0" + extend2 "^1.0.0" + graceful "^1.0.1" + humanize-ms "^1.2.1" + is-type-of "^1.2.0" + koa-bodyparser "^4.2.1" + koa-is-json "^1.0.0" + koa-override "^3.0.0" + ms "^2.1.1" + mz "^2.7.0" + on-finished "^2.3.0" + sendmessage "^1.1.0" + urllib "^2.29.1" + utility "^1.14.0" + ylru "^1.2.1" + +electron-to-chromium@^1.3.47: + version "1.3.70" + resolved "http://registry.npm.taobao.org/electron-to-chromium/download/electron-to-chromium-1.3.70.tgz#ded377256d92d81b4257d36c65aa890274afcfd2" + +email-validator@^1.0.7: + version "1.2.3" + resolved "http://registry.npm.taobao.org/email-validator/download/email-validator-1.2.3.tgz#3e3aa65595e079a4686b41c004485a53638a6ed5" + +emoji-regex@^6.5.1: + version "6.5.1" + resolved "http://registry.npm.taobao.org/emoji-regex/download/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2" + +empower-assert@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/empower-assert/download/empower-assert-1.1.0.tgz#8d327fbe69a88af90dda98d1bfc9829d2a24fd62" + dependencies: + estraverse "^4.2.0" + +empower-core@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/empower-core/download/empower-core-1.2.0.tgz#ce3fb2484d5187fa29c23fba8344b0b2fdf5601c" + dependencies: + call-signature "0.0.2" + core-js "^2.0.0" + +empower@^1.3.1: + version "1.3.1" + resolved "http://registry.npm.taobao.org/empower/download/empower-1.3.1.tgz#768979cbbb36d71d8f5edaab663deacb9dab916c" + dependencies: + core-js "^2.0.0" + empower-core "^1.2.0" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "http://registry.npm.taobao.org/end-of-stream/download/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +end-or-error@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/end-or-error/download/end-or-error-1.0.1.tgz#dc7a6210fe78d372fee24a8b4899dbd155414dcb" + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.2" + resolved "http://registry.npm.taobao.org/error-ex/download/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + dependencies: + is-arrayish "^0.2.1" + +error-inject@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/error-inject/download/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" + +es-abstract@^1.7.0: + version "1.12.0" + resolved "http://registry.npm.taobao.org/es-abstract/download/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + +es-to-primitive@^1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/es-to-primitive/download/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" + dependencies: + is-callable "^1.1.1" + is-date-object "^1.0.1" + is-symbol "^1.0.1" + +es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.46" + resolved "http://registry.npm.taobao.org/es5-ext/download/es5-ext-0.10.46.tgz#efd99f67c5a7ec789baa3daa7f79870388f7f572" + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + next-tick "1" + +es6-error@^4.0.1: + version "4.1.1" + resolved "http://registry.npm.taobao.org/es6-error/download/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" + +es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: + version "2.0.3" + resolved "http://registry.npm.taobao.org/es6-iterator/download/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "http://registry.npm.taobao.org/es6-map/download/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-promise@^4.0.3: + version "4.2.5" + resolved "http://registry.npm.taobao.org/es6-promise/download/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "http://registry.npm.taobao.org/es6-promisify/download/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + dependencies: + es6-promise "^4.0.3" + +es6-set@~0.1.5: + version "0.1.5" + resolved "http://registry.npm.taobao.org/es6-set/download/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "http://registry.npm.taobao.org/es6-symbol/download/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "http://registry.npm.taobao.org/es6-weak-map/download/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + +escallmatch@^1.5.0: + version "1.5.0" + resolved "http://registry.npm.taobao.org/escallmatch/download/escallmatch-1.5.0.tgz#50099d86e8091b092df8ddfbc3f9a6fb05a024d0" + dependencies: + call-matcher "^1.0.0" + esprima "^2.0.0" + +escape-html@^1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/escape-html/download/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.4, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "http://registry.npm.taobao.org/escape-string-regexp/download/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escodegen@1.x.x, escodegen@^1.10.0, escodegen@^1.7.0: + version "1.11.0" + resolved "http://registry.npm.taobao.org/escodegen/download/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +escope@^3.3.0: + version "3.6.0" + resolved "http://registry.npm.taobao.org/escope/download/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-config-egg@^6.0.0: + version "6.0.0" + resolved "http://registry.npm.taobao.org/eslint-config-egg/download/eslint-config-egg-6.0.0.tgz#f71353b0056470e2033f9a9600c7a1b040e4ee8b" + dependencies: + babel-eslint "^8.1.2" + eslint-plugin-import "^2.8.0" + eslint-plugin-jsx-a11y "^6.0.3" + eslint-plugin-react "^7.5.1" + +eslint-import-resolver-node@^0.3.1: + version "0.3.2" + resolved "http://registry.npm.taobao.org/eslint-import-resolver-node/download/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" + dependencies: + debug "^2.6.9" + resolve "^1.5.0" + +eslint-module-utils@^2.2.0: + version "2.2.0" + resolved "http://registry.npm.taobao.org/eslint-module-utils/download/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746" + dependencies: + debug "^2.6.8" + pkg-dir "^1.0.0" + +eslint-plugin-import@^2.8.0: + version "2.14.0" + resolved "http://registry.npm.taobao.org/eslint-plugin-import/download/eslint-plugin-import-2.14.0.tgz#6b17626d2e3e6ad52cfce8807a845d15e22111a8" + dependencies: + contains-path "^0.1.0" + debug "^2.6.8" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.1" + eslint-module-utils "^2.2.0" + has "^1.0.1" + lodash "^4.17.4" + minimatch "^3.0.3" + read-pkg-up "^2.0.0" + resolve "^1.6.0" + +eslint-plugin-jsx-a11y@^6.0.3: + version "6.1.1" + resolved "http://registry.npm.taobao.org/eslint-plugin-jsx-a11y/download/eslint-plugin-jsx-a11y-6.1.1.tgz#7bf56dbe7d47d811d14dbb3ddff644aa656ce8e1" + dependencies: + aria-query "^3.0.0" + array-includes "^3.0.3" + ast-types-flow "^0.0.7" + axobject-query "^2.0.1" + damerau-levenshtein "^1.0.4" + emoji-regex "^6.5.1" + has "^1.0.3" + jsx-ast-utils "^2.0.1" + +eslint-plugin-react@^7.5.1: + version "7.11.1" + resolved "http://registry.npm.taobao.org/eslint-plugin-react/download/eslint-plugin-react-7.11.1.tgz#c01a7af6f17519457d6116aa94fc6d2ccad5443c" + dependencies: + array-includes "^3.0.3" + doctrine "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.0.1" + prop-types "^15.6.2" + +eslint-scope@3.7.1: + version "3.7.1" + resolved "http://registry.npm.taobao.org/eslint-scope/download/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-scope@^3.7.1: + version "3.7.3" + resolved "http://registry.npm.taobao.org/eslint-scope/download/eslint-scope-3.7.3.tgz#bb507200d3d17f60247636160b4826284b108535" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-visitor-keys@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/eslint-visitor-keys/download/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" + +eslint@^4.11.0: + version "4.19.1" + resolved "http://registry.npm.taobao.org/eslint/download/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" + dependencies: + ajv "^5.3.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^3.1.0" + doctrine "^2.1.0" + eslint-scope "^3.7.1" + eslint-visitor-keys "^1.0.0" + espree "^3.5.4" + esquery "^1.0.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^11.0.1" + ignore "^3.3.3" + imurmurhash "^0.1.4" + inquirer "^3.0.6" + is-resolvable "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + regexpp "^1.0.1" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" + strip-json-comments "~2.0.1" + table "4.0.2" + text-table "~0.2.0" + +espower-loader@^1.0.0: + version "1.2.2" + resolved "http://registry.npm.taobao.org/espower-loader/download/espower-loader-1.2.2.tgz#edb46c3c59a06bac8ea73a695c86e5c5a0bc82da" + dependencies: + convert-source-map "^1.1.0" + espower-source "^2.0.0" + minimatch "^3.0.0" + source-map-support "^0.4.0" + xtend "^4.0.0" + +espower-location-detector@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/espower-location-detector/download/espower-location-detector-1.0.0.tgz#a17b7ecc59d30e179e2bef73fb4137704cb331b5" + dependencies: + is-url "^1.2.1" + path-is-absolute "^1.0.0" + source-map "^0.5.0" + xtend "^4.0.0" + +espower-source@^2.0.0, espower-source@^2.3.0: + version "2.3.0" + resolved "http://registry.npm.taobao.org/espower-source/download/espower-source-2.3.0.tgz#43e93b2c18af50018bdb1bea7a1271f4a1c125f4" + dependencies: + acorn "^5.0.0" + acorn-es7-plugin "^1.0.10" + convert-source-map "^1.1.1" + empower-assert "^1.0.0" + escodegen "^1.10.0" + espower "^2.1.1" + estraverse "^4.0.0" + merge-estraverse-visitors "^1.0.0" + multi-stage-sourcemap "^0.2.1" + path-is-absolute "^1.0.0" + xtend "^4.0.0" + +espower-typescript@^9.0.1: + version "9.0.1" + resolved "http://registry.npm.taobao.org/espower-typescript/download/espower-typescript-9.0.1.tgz#536140750f8509f4bdc5db7461c0bf60052c0658" + dependencies: + espower-source "^2.3.0" + minimatch "^3.0.3" + source-map-support "^0.5.9" + ts-node "^7.0.1" + +espower@^2.1.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/espower/download/espower-2.1.1.tgz#158c91585528db46c0eb5a731c2136a427ad2857" + dependencies: + array-find "^1.0.0" + escallmatch "^1.5.0" + escodegen "^1.7.0" + escope "^3.3.0" + espower-location-detector "^1.0.0" + espurify "^1.3.0" + estraverse "^4.1.0" + source-map "^0.5.0" + type-name "^2.0.0" + xtend "^4.0.0" + +espree@^3.5.4: + version "3.5.4" + resolved "http://registry.npm.taobao.org/espree/download/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" + dependencies: + acorn "^5.5.0" + acorn-jsx "^3.0.0" + +esprima@3.x.x, esprima@^3.1.3: + version "3.1.3" + resolved "http://registry.npm.taobao.org/esprima/download/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + +esprima@^2.0.0: + version "2.7.3" + resolved "http://registry.npm.taobao.org/esprima/download/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@^4.0.0: + version "4.0.1" + resolved "http://registry.npm.taobao.org/esprima/download/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + +espurify@^1.3.0, espurify@^1.6.0: + version "1.8.1" + resolved "http://registry.npm.taobao.org/espurify/download/espurify-1.8.1.tgz#5746c6c1ab42d302de10bd1d5bf7f0e8c0515056" + dependencies: + core-js "^2.0.0" + +esquery@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/esquery/download/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "http://registry.npm.taobao.org/esrecurse/download/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + dependencies: + estraverse "^4.1.0" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "http://registry.npm.taobao.org/estraverse/download/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "http://registry.npm.taobao.org/esutils/download/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +event-emitter@~0.3.5: + version "0.3.5" + resolved "http://registry.npm.taobao.org/event-emitter/download/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + +event-stream@^3.3.4: + version "3.3.6" + resolved "http://registry.npm.taobao.org/event-stream/download/event-stream-3.3.6.tgz#cac1230890e07e73ec9cacd038f60a5b66173eef" + dependencies: + duplexer "^0.1.1" + flatmap-stream "^0.1.0" + from "^0.1.7" + map-stream "0.0.7" + pause-stream "^0.0.11" + split "^1.0.1" + stream-combiner "^0.2.2" + through "^2.3.8" + +execa@^0.7.0: + version "0.7.0" + resolved "http://registry.npm.taobao.org/execa/download/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "http://registry.npm.taobao.org/expand-brackets/download/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/extend-shallow/download/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "http://registry.npm.taobao.org/extend-shallow/download/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend2@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/extend2/download/extend2-1.0.0.tgz#0425a989b4dac2a486a32257f5140103756a7a3c" + +extend@3, extend@^3.0.0, extend@^3.0.1: + version "3.0.2" + resolved "http://registry.npm.taobao.org/extend/download/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + +external-editor@^2.0.4: + version "2.2.0" + resolved "http://registry.npm.taobao.org/external-editor/download/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" + dependencies: + chardet "^0.4.0" + iconv-lite "^0.4.17" + tmp "^0.0.33" + +external-editor@^3.0.0: + version "3.0.3" + resolved "http://registry.npm.taobao.org/external-editor/download/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extglob@^2.0.4: + version "2.0.4" + resolved "http://registry.npm.taobao.org/extglob/download/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/fast-deep-equal/download/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-glob@^2.0.2: + version "2.2.2" + resolved "http://registry.npm.taobao.org/fast-glob/download/fast-glob-2.2.2.tgz#71723338ac9b4e0e2fff1d6748a2a13d5ed352bf" + dependencies: + "@mrmlnc/readdir-enhanced" "^2.2.1" + "@nodelib/fs.stat" "^1.0.1" + glob-parent "^3.1.0" + is-glob "^4.0.0" + merge2 "^1.2.1" + micromatch "^3.1.10" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/fast-json-stable-stringify/download/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "http://registry.npm.taobao.org/fast-levenshtein/download/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/fd-slicer/download/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + dependencies: + pend "~1.2.0" + +figures@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/figures/download/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/file-entry-cache/download/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +file-uri-to-path@1: + version "1.0.0" + resolved "http://registry.npm.taobao.org/file-uri-to-path/download/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + +fill-range@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/fill-range/download/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +find-cache-dir@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^3.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "http://registry.npm.taobao.org/find-up/download/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/find-up/download/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/find-up/download/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + dependencies: + locate-path "^3.0.0" + +flat-cache@^1.2.1: + version "1.3.0" + resolved "http://registry.npm.taobao.org/flat-cache/download/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +flatmap-stream@^0.1.0: + version "0.1.0" + resolved "http://registry.npm.taobao.org/flatmap-stream/download/flatmap-stream-0.1.0.tgz#ed54e01422cd29281800914fcb968d58b685d5f1" + +flexbuffer@0.0.6: + version "0.0.6" + resolved "http://registry.npm.taobao.org/flexbuffer/download/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30" + +for-in@^1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/for-in/download/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +foreground-child@^1.5.6: + version "1.5.6" + resolved "http://registry.npm.taobao.org/foreground-child/download/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9" + dependencies: + cross-spawn "^4" + signal-exit "^3.0.0" + +form-data@^2.3.1: + version "2.3.2" + resolved "http://registry.npm.taobao.org/form-data/download/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + +formidable@^1.2.0: + version "1.2.1" + resolved "http://registry.npm.taobao.org/formidable/download/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "http://registry.npm.taobao.org/fragment-cache/download/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + dependencies: + map-cache "^0.2.2" + +fresh@~0.5.2: + version "0.5.2" + resolved "http://registry.npm.taobao.org/fresh/download/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + +from2@^2.1.1: + version "2.3.0" + resolved "http://registry.npm.taobao.org/from2/download/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +from@^0.1.7: + version "0.1.7" + resolved "http://registry.npm.taobao.org/from/download/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + +fs-minipass@^1.2.5: + version "1.2.5" + resolved "http://registry.npm.taobao.org/fs-minipass/download/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + dependencies: + minipass "^2.2.1" + +fs-readdir-recursive@^1.0.0, fs-readdir-recursive@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/fs-readdir-recursive/download/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/fs.realpath/download/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.2.2: + version "1.2.4" + resolved "http://registry.npm.taobao.org/fsevents/download/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" + dependencies: + nan "^2.9.2" + node-pre-gyp "^0.10.0" + +ftp@~0.3.10: + version "0.3.10" + resolved "http://registry.npm.taobao.org/ftp/download/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" + dependencies: + readable-stream "1.1.x" + xregexp "2.0.0" + +function-bind@^1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/function-bind/download/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/functional-red-black-tree/download/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + +gauge@~2.7.3: + version "2.7.4" + resolved "http://registry.npm.taobao.org/gauge/download/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +geoip-lite@^1.3.2: + version "1.3.2" + resolved "http://registry.npm.taobao.org/geoip-lite/download/geoip-lite-1.3.2.tgz#2d2cb6cb5a1cba0017d058d2ac3e67e368c415ba" + dependencies: + async "^2.1.1" + colors "^1.1.2" + glob "^7.1.1" + iconv-lite "^0.4.13" + ip-address "^5.8.9" + lazy "^1.0.11" + rimraf "^2.5.2" + save "^2.3.2" + yauzl "^2.9.2" + +get-caller-file@^1.0.1: + version "1.0.3" + resolved "http://registry.npm.taobao.org/get-caller-file/download/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + +get-pkg-repo@^1.0.0: + version "1.4.0" + resolved "http://registry.npm.taobao.org/get-pkg-repo/download/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" + dependencies: + hosted-git-info "^2.1.4" + meow "^3.3.0" + normalize-package-data "^2.3.0" + parse-github-repo-url "^1.3.0" + through2 "^2.0.0" + +get-ready@^1.0.0, get-ready@~1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/get-ready/download/get-ready-1.0.0.tgz#f91817f1e9adecfea13a562adfc8de883ab34782" + +get-ready@^2.0.0, get-ready@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/get-ready/download/get-ready-2.0.1.tgz#a48c418753e39cf4d01f3a420cf1b757ddcc648f" + dependencies: + is-type-of "^1.0.0" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "http://registry.npm.taobao.org/get-stdin/download/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stream@3.0.0, get-stream@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/get-stream/download/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +get-uri@^2.0.0: + version "2.0.2" + resolved "http://registry.npm.taobao.org/get-uri/download/get-uri-2.0.2.tgz#5c795e71326f6ca1286f2fc82575cd2bab2af578" + dependencies: + data-uri-to-buffer "1" + debug "2" + extend "3" + file-uri-to-path "1" + ftp "~0.3.10" + readable-stream "2" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "http://registry.npm.taobao.org/get-value/download/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + +git-raw-commits@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/git-raw-commits/download/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" + dependencies: + dargs "^4.0.1" + lodash.template "^4.0.2" + meow "^4.0.0" + split2 "^2.0.0" + through2 "^2.0.0" + +git-remote-origin-url@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/git-remote-origin-url/download/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" + dependencies: + gitconfiglocal "^1.0.0" + pify "^2.3.0" + +git-semver-tags@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/git-semver-tags/download/git-semver-tags-2.0.0.tgz#c218fd895bdf8e8e02f6bde555b2c3893ac73cd7" + dependencies: + meow "^4.0.0" + semver "^5.5.0" + +gitconfiglocal@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/gitconfiglocal/download/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" + dependencies: + ini "^1.3.2" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/glob-parent/download/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-to-regexp@^0.3.0: + version "0.3.0" + resolved "http://registry.npm.taobao.org/glob-to-regexp/download/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + +glob@7.1.2: + version "7.1.2" + resolved "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: + version "7.1.3" + resolved "http://registry.npm.taobao.org/glob/download/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^0.1.0: + version "0.1.1" + resolved "http://registry.npm.taobao.org/global-dirs/download/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + dependencies: + ini "^1.3.4" + +globals@^11.0.1, globals@^11.1.0: + version "11.7.0" + resolved "http://registry.npm.taobao.org/globals/download/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673" + +globals@^9.18.0: + version "9.18.0" + resolved "http://registry.npm.taobao.org/globals/download/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +globby@8.0.1, globby@^8.0.1: + version "8.0.1" + resolved "http://registry.npm.taobao.org/globby/download/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50" + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + fast-glob "^2.0.2" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + +globby@^5.0.0: + version "5.0.0" + resolved "http://registry.npm.taobao.org/globby/download/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +got@8.3.2: + version "8.3.2" + resolved "http://registry.npm.taobao.org/got/download/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" + dependencies: + "@sindresorhus/is" "^0.7.0" + cacheable-request "^2.1.1" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + into-stream "^3.1.0" + is-retry-allowed "^1.1.0" + isurl "^1.0.0-alpha5" + lowercase-keys "^1.0.0" + mimic-response "^1.0.0" + p-cancelable "^0.4.0" + p-timeout "^2.0.1" + pify "^3.0.0" + safe-buffer "^5.1.1" + timed-out "^4.0.1" + url-parse-lax "^3.0.0" + url-to-options "^1.0.1" + +got@^6.7.1: + version "6.7.1" + resolved "http://registry.npm.taobao.org/got/download/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + dependencies: + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" + url-parse-lax "^1.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2: + version "4.1.11" + resolved "http://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +graceful-process@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/graceful-process/download/graceful-process-1.2.0.tgz#5b2bd6eda3b59777db6a8a9332e79e6246086d85" + dependencies: + is-type-of "^1.2.0" + once "^1.4.0" + +graceful@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/graceful/download/graceful-1.0.1.tgz#a7a62c8fffb3fab89b6d0d32f6e4074d32a1eeb2" + dependencies: + humanize-ms "^1.2.0" + +gravatar@^1.6.0: + version "1.6.0" + resolved "http://registry.npm.taobao.org/gravatar/download/gravatar-1.6.0.tgz#8bdc9b786ca725a8e7076416d1731f8d3331c976" + dependencies: + blueimp-md5 "^2.3.0" + email-validator "^1.0.7" + querystring "0.2.0" + yargs "^6.0.0" + +growl@1.10.5: + version "1.10.5" + resolved "http://registry.npm.taobao.org/growl/download/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + +handlebars@^4.0.11, handlebars@^4.0.2: + version "4.0.12" + resolved "http://registry.npm.taobao.org/handlebars/download/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" + dependencies: + async "^2.5.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/has-ansi/download/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/has-flag/download/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +has-symbol-support-x@^1.4.1: + version "1.4.2" + resolved "http://registry.npm.taobao.org/has-symbol-support-x/download/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" + +has-to-string-tag-x@^1.2.0: + version "1.4.1" + resolved "http://registry.npm.taobao.org/has-to-string-tag-x/download/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" + dependencies: + has-symbol-support-x "^1.4.1" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "http://registry.npm.taobao.org/has-unicode/download/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has-value@^0.3.1: + version "0.3.1" + resolved "http://registry.npm.taobao.org/has-value/download/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/has-value/download/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "http://registry.npm.taobao.org/has-values/download/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + +has-values@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/has-values/download/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/has/download/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + dependencies: + function-bind "^1.1.1" + +he@1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/he/download/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +header-case@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/header-case/download/header-case-1.0.1.tgz#9535973197c144b09613cd65d317ef19963bd02d" + dependencies: + no-case "^2.2.0" + upper-case "^1.1.3" + +highlight.js@^9.12.0: + version "9.12.0" + resolved "http://registry.npm.taobao.org/highlight.js/download/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/home-or-tmp/download/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +hosted-git-info@^2.1.4: + version "2.7.1" + resolved "http://registry.npm.taobao.org/hosted-git-info/download/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" + +http-assert@^1.3.0: + version "1.4.0" + resolved "http://registry.npm.taobao.org/http-assert/download/http-assert-1.4.0.tgz#0e550b4fca6adf121bbeed83248c17e62f593a9a" + dependencies: + deep-equal "~1.0.1" + http-errors "~1.7.1" + +http-cache-semantics@3.8.1: + version "3.8.1" + resolved "http://registry.npm.taobao.org/http-cache-semantics/download/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" + +http-errors@1.6.3: + version "1.6.3" + resolved "http://registry.npm.taobao.org/http-errors/download/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-errors@^1.3.1, http-errors@^1.6.3, http-errors@~1.7.1: + version "1.7.1" + resolved "http://registry.npm.taobao.org/http-errors/download/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027" + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-proxy-agent@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/http-proxy-agent/download/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + dependencies: + agent-base "4" + debug "3.1.0" + +https-proxy-agent@^2.2.0, https-proxy-agent@^2.2.1: + version "2.2.1" + resolved "http://registry.npm.taobao.org/https-proxy-agent/download/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" + dependencies: + agent-base "^4.1.0" + debug "^3.1.0" + +humanize-bytes@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/humanize-bytes/download/humanize-bytes-1.0.1.tgz#21f57ca318d211a006dc9798a46584faf2d97e9c" + dependencies: + bytes "~2.2.0" + +humanize-ms@^1.2.0, humanize-ms@^1.2.1: + version "1.2.1" + resolved "http://registry.npm.taobao.org/humanize-ms/download/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + dependencies: + ms "^2.0.0" + +iconv-lite@0.4.23: + version "0.4.23" + resolved "http://registry.npm.taobao.org/iconv-lite/download/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.4.13, iconv-lite@^0.4.15, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4: + version "0.4.24" + resolved "http://registry.npm.taobao.org/iconv-lite/download/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.4: + version "1.1.12" + resolved "http://registry.npm.taobao.org/ieee754/download/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "http://registry.npm.taobao.org/ignore-walk/download/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + dependencies: + minimatch "^3.0.4" + +ignore@^3.3.3, ignore@^3.3.5: + version "3.3.10" + resolved "http://registry.npm.taobao.org/ignore/download/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + +import-lazy@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/import-lazy/download/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "http://registry.npm.taobao.org/imurmurhash/download/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indent-string@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/indent-string/download/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indent-string@^3.0.0: + version "3.2.0" + resolved "http://registry.npm.taobao.org/indent-string/download/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + +indexof@0.0.1: + version "0.0.1" + resolved "http://registry.npm.taobao.org/indexof/download/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflation@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/inflation/download/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f" + +inflection@^1.12.0: + version "1.12.0" + resolved "http://registry.npm.taobao.org/inflection/download/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" + +inflight@^1.0.4: + version "1.0.6" + resolved "http://registry.npm.taobao.org/inflight/download/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "http://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: + version "1.3.5" + resolved "http://registry.npm.taobao.org/ini/download/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + +inquirer@6.2.0: + version "6.2.0" + resolved "http://registry.npm.taobao.org/inquirer/download/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.0" + figures "^2.0.0" + lodash "^4.17.10" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.1.0" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +inquirer@^3.0.6: + version "3.3.0" + resolved "http://registry.npm.taobao.org/inquirer/download/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.4" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +inspector-proxy@^1.2.1: + version "1.2.1" + resolved "http://registry.npm.taobao.org/inspector-proxy/download/inspector-proxy-1.2.1.tgz#870d2f419150178b5c3765b2852ae7289be54079" + dependencies: + cfork "^1.6.1" + debug "^3.0.1" + tcp-proxy.js "^1.0.5" + urllib "^2.24.0" + +intelli-espower-loader@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/intelli-espower-loader/download/intelli-espower-loader-1.0.1.tgz#2c7b03146bc1d46bf210d0a0397c5c91ab4ca2b0" + dependencies: + espower-loader "^1.0.0" + +interpret@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/interpret/download/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" + +into-stream@^3.1.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/into-stream/download/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" + dependencies: + from2 "^2.1.1" + p-is-promise "^1.1.0" + +invariant@^2.2.0, invariant@^2.2.2: + version "2.2.4" + resolved "http://registry.npm.taobao.org/invariant/download/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/invert-kv/download/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +ioredis@^3.2.2: + version "3.2.2" + resolved "http://registry.npm.taobao.org/ioredis/download/ioredis-3.2.2.tgz#b7d5ff3afd77bb9718bb2821329b894b9a44c00b" + dependencies: + bluebird "^3.3.4" + cluster-key-slot "^1.0.6" + debug "^2.6.9" + denque "^1.1.0" + flexbuffer "0.0.6" + lodash.assign "^4.2.0" + lodash.bind "^4.2.1" + lodash.clone "^4.5.0" + lodash.clonedeep "^4.5.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.foreach "^4.5.0" + lodash.isempty "^4.4.0" + lodash.keys "^4.2.0" + lodash.noop "^3.0.1" + lodash.partial "^4.2.1" + lodash.pick "^4.4.0" + lodash.sample "^4.2.1" + lodash.shuffle "^4.2.0" + lodash.values "^4.3.0" + redis-commands "^1.2.0" + redis-parser "^2.4.0" + +ip-address@^5.8.9: + version "5.8.9" + resolved "http://registry.npm.taobao.org/ip-address/download/ip-address-5.8.9.tgz#6379277c23fc5adb20511e4d23ec2c1bde105dfd" + dependencies: + jsbn "1.1.0" + lodash.find "^4.6.0" + lodash.max "^4.0.1" + lodash.merge "^4.6.0" + lodash.padstart "^4.6.1" + lodash.repeat "^4.1.0" + sprintf-js "1.1.0" + +ip@^1.1.4, ip@^1.1.5: + version "1.1.5" + resolved "http://registry.npm.taobao.org/ip/download/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "http://registry.npm.taobao.org/is-accessor-descriptor/download/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/is-accessor-descriptor/download/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "http://registry.npm.taobao.org/is-arrayish/download/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/is-binary-path/download/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5, is-buffer@~1.1.1: + version "1.1.6" + resolved "http://registry.npm.taobao.org/is-buffer/download/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/is-builtin-module/download/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-callable@^1.1.1, is-callable@^1.1.3: + version "1.1.4" + resolved "http://registry.npm.taobao.org/is-callable/download/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + +is-ci@1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/is-ci/download/is-ci-1.2.0.tgz#3f4a08d6303a09882cef3f0fb97439c5f5ce2d53" + dependencies: + ci-info "^1.3.0" + +is-ci@^1.0.10: + version "1.2.1" + resolved "http://registry.npm.taobao.org/is-ci/download/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" + dependencies: + ci-info "^1.5.0" + +is-class@~0.0.4: + version "0.0.5" + resolved "http://registry.npm.taobao.org/is-class/download/is-class-0.0.5.tgz#220a8465d24f9a142082d06c7c56edb39052416d" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "http://registry.npm.taobao.org/is-data-descriptor/download/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/is-data-descriptor/download/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/is-date-object/download/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "http://registry.npm.taobao.org/is-descriptor/download/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/is-descriptor/download/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "http://registry.npm.taobao.org/is-extendable/download/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extendable@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/is-extendable/download/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/is-extglob/download/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + +is-finite@^1.0.0: + version "1.0.2" + resolved "http://registry.npm.taobao.org/is-finite/download/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-generator-function@^1.0.7: + version "1.0.7" + resolved "http://registry.npm.taobao.org/is-generator-function/download/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + +is-generator@^1.0.1: + version "1.0.3" + resolved "http://registry.npm.taobao.org/is-generator/download/is-generator-1.0.3.tgz#c14c21057ed36e328db80347966c693f886389f3" + +is-glob@^3.1.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/is-glob/download/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/is-glob/download/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "http://registry.npm.taobao.org/is-installed-globally/download/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" + +is-lower-case@^1.1.0: + version "1.1.3" + resolved "http://registry.npm.taobao.org/is-lower-case/download/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393" + dependencies: + lower-case "^1.1.0" + +is-nan@^1.2.1: + version "1.2.1" + resolved "http://registry.npm.taobao.org/is-nan/download/is-nan-1.2.1.tgz#9faf65b6fb6db24b7f5c0628475ea71f988401e2" + dependencies: + define-properties "^1.1.1" + +is-npm@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/is-npm/download/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + +is-number@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/is-number/download/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-obj@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/is-obj/download/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-object@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/is-object/download/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/is-path-cwd/download/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/is-path-in-cwd/download/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/is-path-inside/download/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + dependencies: + path-is-inside "^1.0.1" + +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/is-plain-obj/download/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "http://registry.npm.taobao.org/is-plain-object/download/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + +is-promise@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/is-promise/download/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-redirect@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/is-redirect/download/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + +is-regex@^1.0.4: + version "1.0.4" + resolved "http://registry.npm.taobao.org/is-regex/download/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + dependencies: + has "^1.0.1" + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/is-resolvable/download/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + +is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/is-retry-allowed/download/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" + +is-stream@^1.0.0, is-stream@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/is-stream/download/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-subset@^0.1.1: + version "0.1.1" + resolved "http://registry.npm.taobao.org/is-subset/download/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + +is-symbol@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/is-symbol/download/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" + +is-text-path@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/is-text-path/download/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" + dependencies: + text-extensions "^1.0.0" + +is-type-of@^1.0.0, is-type-of@^1.1.0, is-type-of@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/is-type-of/download/is-type-of-1.2.0.tgz#b553cbb4621adf5b4171e8883f370e7a6ec38995" + dependencies: + core-util-is "^1.0.2" + is-class "~0.0.4" + isstream "~0.1.2" + +is-upper-case@^1.1.0: + version "1.1.2" + resolved "http://registry.npm.taobao.org/is-upper-case/download/is-upper-case-1.1.2.tgz#8d0b1fa7e7933a1e58483600ec7d9661cbaf756f" + dependencies: + upper-case "^1.1.0" + +is-url@^1.2.1: + version "1.2.4" + resolved "http://registry.npm.taobao.org/is-url/download/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "http://registry.npm.taobao.org/is-utf8/download/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-windows@^1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/is-windows/download/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + +isarray@0.0.1: + version "0.0.1" + resolved "http://registry.npm.taobao.org/isarray/download/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/isexe/download/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/isobject/download/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "http://registry.npm.taobao.org/isobject/download/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + +isstream@~0.1.2: + version "0.1.2" + resolved "http://registry.npm.taobao.org/isstream/download/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +istanbul-lib-coverage@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/istanbul-lib-coverage/download/istanbul-lib-coverage-2.0.1.tgz#2aee0e073ad8c5f6a0b00e0dfbf52b4667472eda" + +istanbul-lib-hook@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/istanbul-lib-hook/download/istanbul-lib-hook-2.0.1.tgz#918a57b75a0f951d552a08487ca1fa5336433d72" + dependencies: + append-transform "^1.0.0" + +istanbul-lib-instrument@^2.3.2: + version "2.3.2" + resolved "http://registry.npm.taobao.org/istanbul-lib-instrument/download/istanbul-lib-instrument-2.3.2.tgz#b287cbae2b5f65f3567b05e2e29b275eaf92d25e" + dependencies: + "@babel/generator" "7.0.0-beta.51" + "@babel/parser" "7.0.0-beta.51" + "@babel/template" "7.0.0-beta.51" + "@babel/traverse" "7.0.0-beta.51" + "@babel/types" "7.0.0-beta.51" + istanbul-lib-coverage "^2.0.1" + semver "^5.5.0" + +istanbul-lib-report@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/istanbul-lib-report/download/istanbul-lib-report-2.0.1.tgz#64a0a08f42676b9c801b841b9dc3311017c6ae09" + dependencies: + istanbul-lib-coverage "^2.0.1" + make-dir "^1.3.0" + supports-color "^5.4.0" + +istanbul-lib-source-maps@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/istanbul-lib-source-maps/download/istanbul-lib-source-maps-2.0.1.tgz#ce8b45131d8293fdeaa732f4faf1852d13d0a97e" + dependencies: + debug "^3.1.0" + istanbul-lib-coverage "^2.0.1" + make-dir "^1.3.0" + rimraf "^2.6.2" + source-map "^0.6.1" + +istanbul-reports@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/istanbul-reports/download/istanbul-reports-2.0.0.tgz#eb12eddf55724ebc557b32cd77c34d11ed7980c1" + dependencies: + handlebars "^4.0.11" + +isurl@^1.0.0-alpha5: + version "1.0.0" + resolved "http://registry.npm.taobao.org/isurl/download/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" + dependencies: + has-to-string-tag-x "^1.2.0" + is-object "^1.0.1" + +jest-changed-files@^23.4.2: + version "23.4.2" + resolved "http://registry.npm.taobao.org/jest-changed-files/download/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83" + dependencies: + throat "^4.0.0" + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "http://registry.npm.taobao.org/js-tokens/download/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "http://registry.npm.taobao.org/js-tokens/download/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + +js-yaml@^3.9.1: + version "3.12.0" + resolved "http://registry.npm.taobao.org/js-yaml/download/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/jsbn/download/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + +jsesc@^1.3.0: + version "1.3.0" + resolved "http://registry.npm.taobao.org/jsesc/download/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@^2.5.1: + version "2.5.1" + resolved "http://registry.npm.taobao.org/jsesc/download/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" + +jsesc@~0.5.0: + version "0.5.0" + resolved "http://registry.npm.taobao.org/jsesc/download/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-buffer@3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/json-buffer/download/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "http://registry.npm.taobao.org/json-parse-better-errors/download/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "http://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/json-stable-stringify-without-jsonify/download/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "http://registry.npm.taobao.org/json-stringify-safe/download/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@^0.5.1: + version "0.5.1" + resolved "http://registry.npm.taobao.org/json5/download/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonp-body@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/jsonp-body/download/jsonp-body-1.0.0.tgz#e610fb6fcea79cf0cc9f27baa7b56377d4b0bb36" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "http://registry.npm.taobao.org/jsonparse/download/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + +jsonwebtoken@^8.3.0: + version "8.3.0" + resolved "http://registry.npm.taobao.org/jsonwebtoken/download/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643" + dependencies: + jws "^3.1.5" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + +jstoxml@^0.2.3: + version "0.2.4" + resolved "http://registry.npm.taobao.org/jstoxml/download/jstoxml-0.2.4.tgz#ff3fb67856883a032953c7ce8ce7486210f48447" + +jsx-ast-utils@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/jsx-ast-utils/download/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f" + dependencies: + array-includes "^3.0.3" + +jwa@^1.1.5: + version "1.1.6" + resolved "http://registry.npm.taobao.org/jwa/download/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.1.5" + resolved "http://registry.npm.taobao.org/jws/download/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + +kareem@2.2.1: + version "2.2.1" + resolved "http://registry.npm.taobao.org/kareem/download/kareem-2.2.1.tgz#9950809415aa3cde62ab43b4f7b919d99816e015" + +keygrip@~1.0.2: + version "1.0.3" + resolved "http://registry.npm.taobao.org/keygrip/download/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" + +keyv@3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/keyv/download/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" + dependencies: + json-buffer "3.0.0" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "http://registry.npm.taobao.org/kind-of/download/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/kind-of/download/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "http://registry.npm.taobao.org/kind-of/download/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "http://registry.npm.taobao.org/kind-of/download/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + +ko-sleep@^1.0.2, ko-sleep@^1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/ko-sleep/download/ko-sleep-1.0.3.tgz#28a2a0a1485e8b7f415ff488dee17d24788ab082" + dependencies: + ms "^2.0.0" + +koa-bodyparser@^4.2.1: + version "4.2.1" + resolved "http://registry.npm.taobao.org/koa-bodyparser/download/koa-bodyparser-4.2.1.tgz#4d7dacb5e6db1106649b595d9e5ccb158b6f3b29" + dependencies: + co-body "^6.0.0" + copy-to "^2.0.1" + +koa-compose@^3.0.0: + version "3.2.1" + resolved "http://registry.npm.taobao.org/koa-compose/download/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" + dependencies: + any-promise "^1.1.0" + +koa-compose@^4.0.0, koa-compose@^4.1.0: + version "4.1.0" + resolved "http://registry.npm.taobao.org/koa-compose/download/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" + +koa-convert@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/koa-convert/download/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" + dependencies: + co "^4.6.0" + koa-compose "^3.0.0" + +koa-is-json@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/koa-is-json/download/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" + +koa-locales@^1.7.0: + version "1.8.0" + resolved "http://registry.npm.taobao.org/koa-locales/download/koa-locales-1.8.0.tgz#e8762e937007a0baf9092ea24c44fab860e394bd" + dependencies: + debug "^2.6.0" + humanize-ms "^1.2.0" + ini "^1.3.4" + object-assign "^4.1.0" + +koa-onerror@^4.0.0: + version "4.1.0" + resolved "http://registry.npm.taobao.org/koa-onerror/download/koa-onerror-4.1.0.tgz#7949c7651941e67b11813bf1fad03c2d34470b1c" + dependencies: + escape-html "^1.0.3" + stream-wormhole "^1.1.0" + +koa-override@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/koa-override/download/koa-override-3.0.0.tgz#a14fa84975bab08c5730a43788883164f4f81a1c" + dependencies: + methods "^1.1.2" + +koa-range@^0.3.0: + version "0.3.0" + resolved "http://registry.npm.taobao.org/koa-range/download/koa-range-0.3.0.tgz#3588e3496473a839a1bd264d2a42b1d85bd7feac" + dependencies: + stream-slice "^0.1.2" + +koa-router@^7.4.0: + version "7.4.0" + resolved "http://registry.npm.taobao.org/koa-router/download/koa-router-7.4.0.tgz#aee1f7adc02d5cb31d7d67465c9eacc825e8c5e0" + dependencies: + debug "^3.1.0" + http-errors "^1.3.1" + koa-compose "^3.0.0" + methods "^1.0.1" + path-to-regexp "^1.1.1" + urijs "^1.19.0" + +koa-session@^5.7.0: + version "5.9.0" + resolved "http://registry.npm.taobao.org/koa-session/download/koa-session-5.9.0.tgz#e0283fa81999afe25f7c7979f3dec2b8a26ac005" + dependencies: + crc "^3.4.4" + debug "^3.1.0" + is-type-of "^1.0.0" + pedding "^1.1.0" + uid-safe "^2.1.3" + +koa-static-cache@^5.1.1: + version "5.1.2" + resolved "http://registry.npm.taobao.org/koa-static-cache/download/koa-static-cache-5.1.2.tgz#49b592007157b164f5e9df5b276e305c8be5016a" + dependencies: + compressible "^2.0.6" + debug "^3.1.0" + fs-readdir-recursive "^1.0.0" + mime-types "^2.1.8" + mz "^2.7.0" + +koa@^2.5.2: + version "2.5.3" + resolved "http://registry.npm.taobao.org/koa/download/koa-2.5.3.tgz#0b0c37eee3aac807a0a6ad36bc0b8660f12d83f1" + dependencies: + accepts "^1.3.5" + cache-content-type "^1.0.0" + content-disposition "~0.5.2" + content-type "^1.0.4" + cookies "~0.7.1" + debug "~3.1.0" + delegates "^1.0.0" + depd "^1.1.2" + destroy "^1.0.4" + error-inject "^1.0.0" + escape-html "^1.0.3" + fresh "~0.5.2" + http-assert "^1.3.0" + http-errors "^1.6.3" + is-generator-function "^1.0.7" + koa-compose "^4.1.0" + koa-convert "^1.2.0" + koa-is-json "^1.0.0" + on-finished "^2.3.0" + only "~0.0.2" + parseurl "^1.3.2" + statuses "^1.5.0" + type-is "^1.6.16" + vary "^1.1.2" + +latest-version@^3.0.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/latest-version/download/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" + dependencies: + package-json "^4.0.0" + +lazy@^1.0.11: + version "1.0.11" + resolved "http://registry.npm.taobao.org/lazy/download/lazy-1.0.11.tgz#daa068206282542c088288e975c297c1ae77b690" + +lcid@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/lcid/download/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "http://registry.npm.taobao.org/levn/download/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/load-json-file/download/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/load-json-file/download/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/load-json-file/download/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/locate-path/download/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/locate-path/download/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash._reinterpolate@~3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/lodash._reinterpolate/download/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash.assign@^4.2.0: + version "4.2.0" + resolved "http://registry.npm.taobao.org/lodash.assign/download/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.bind@^4.2.1: + version "4.2.1" + resolved "http://registry.npm.taobao.org/lodash.bind/download/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" + +lodash.clone@^4.5.0: + version "4.5.0" + resolved "http://registry.npm.taobao.org/lodash.clone/download/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "http://registry.npm.taobao.org/lodash.clonedeep/download/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "http://registry.npm.taobao.org/lodash.debounce/download/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "http://registry.npm.taobao.org/lodash.defaults/download/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "http://registry.npm.taobao.org/lodash.difference/download/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + +lodash.find@^4.6.0: + version "4.6.0" + resolved "http://registry.npm.taobao.org/lodash.find/download/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1" + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "http://registry.npm.taobao.org/lodash.flatten/download/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "http://registry.npm.taobao.org/lodash.flattendeep/download/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + +lodash.foreach@^4.5.0: + version "4.5.0" + resolved "http://registry.npm.taobao.org/lodash.foreach/download/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + +lodash.get@4.4.2: + version "4.4.2" + resolved "http://registry.npm.taobao.org/lodash.get/download/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "http://registry.npm.taobao.org/lodash.includes/download/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "http://registry.npm.taobao.org/lodash.isboolean/download/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + +lodash.isempty@^4.4.0: + version "4.4.0" + resolved "http://registry.npm.taobao.org/lodash.isempty/download/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "http://registry.npm.taobao.org/lodash.isinteger/download/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "http://registry.npm.taobao.org/lodash.isnumber/download/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "http://registry.npm.taobao.org/lodash.isplainobject/download/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "http://registry.npm.taobao.org/lodash.isstring/download/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + +lodash.keys@^4.2.0: + version "4.2.0" + resolved "http://registry.npm.taobao.org/lodash.keys/download/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205" + +lodash.max@^4.0.1: + version "4.0.1" + resolved "http://registry.npm.taobao.org/lodash.max/download/lodash.max-4.0.1.tgz#8735566c618b35a9f760520b487ae79658af136a" + +lodash.merge@^4.6.0: + version "4.6.1" + resolved "http://registry.npm.taobao.org/lodash.merge/download/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" + +lodash.noop@^3.0.1: + version "3.0.1" + resolved "http://registry.npm.taobao.org/lodash.noop/download/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" + +lodash.once@^4.0.0: + version "4.1.1" + resolved "http://registry.npm.taobao.org/lodash.once/download/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + +lodash.padstart@^4.6.1: + version "4.6.1" + resolved "http://registry.npm.taobao.org/lodash.padstart/download/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" + +lodash.partial@^4.2.1: + version "4.2.1" + resolved "http://registry.npm.taobao.org/lodash.partial/download/lodash.partial-4.2.1.tgz#49f3d8cfdaa3bff8b3a91d127e923245418961d4" + +lodash.pick@^4.4.0: + version "4.4.0" + resolved "http://registry.npm.taobao.org/lodash.pick/download/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + +lodash.repeat@^4.1.0: + version "4.1.0" + resolved "http://registry.npm.taobao.org/lodash.repeat/download/lodash.repeat-4.1.0.tgz#fc7de8131d8c8ac07e4b49f74ffe829d1f2bec44" + +lodash.sample@^4.2.1: + version "4.2.1" + resolved "http://registry.npm.taobao.org/lodash.sample/download/lodash.sample-4.2.1.tgz#5e4291b0c753fa1abeb0aab8fb29df1b66f07f6d" + +lodash.shuffle@^4.2.0: + version "4.2.0" + resolved "http://registry.npm.taobao.org/lodash.shuffle/download/lodash.shuffle-4.2.0.tgz#145b5053cf875f6f5c2a33f48b6e9948c6ec7b4b" + +lodash.template@^4.0.2: + version "4.4.0" + resolved "http://registry.npm.taobao.org/lodash.template/download/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" + dependencies: + lodash._reinterpolate "~3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.1.0" + resolved "http://registry.npm.taobao.org/lodash.templatesettings/download/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" + dependencies: + lodash._reinterpolate "~3.0.0" + +lodash.values@^4.3.0: + version "4.3.0" + resolved "http://registry.npm.taobao.org/lodash.values/download/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" + +lodash@4.17.10: + version "4.17.10" + resolved "http://registry.npm.taobao.org/lodash/download/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + +lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0: + version "4.17.11" + resolved "http://registry.npm.taobao.org/lodash/download/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + +log-symbols@^2.2.0: + version "2.2.0" + resolved "http://registry.npm.taobao.org/log-symbols/download/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + dependencies: + chalk "^2.0.1" + +long@^3.2.0: + version "3.2.0" + resolved "http://registry.npm.taobao.org/long/download/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" + +long@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/long/download/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + +loose-envify@^1.0.0, loose-envify@^1.3.1: + version "1.4.0" + resolved "http://registry.npm.taobao.org/loose-envify/download/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "http://registry.npm.taobao.org/loud-rejection/download/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lower-case-first@^1.0.0: + version "1.0.2" + resolved "http://registry.npm.taobao.org/lower-case-first/download/lower-case-first-1.0.2.tgz#e5da7c26f29a7073be02d52bac9980e5922adfa1" + dependencies: + lower-case "^1.1.2" + +lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: + version "1.1.4" + resolved "http://registry.npm.taobao.org/lower-case/download/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + +lowercase-keys@1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/lowercase-keys/download/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + +lowercase-keys@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/lowercase-keys/download/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + +lru-cache@^4.0.1, lru-cache@^4.1.2: + version "4.1.3" + resolved "http://registry.npm.taobao.org/lru-cache/download/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +macos-release@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/macos-release/download/macos-release-1.1.0.tgz#831945e29365b470aa8724b0ab36c8f8959d10fb" + +make-dir@^1.0.0, make-dir@^1.3.0: + version "1.3.0" + resolved "http://registry.npm.taobao.org/make-dir/download/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + dependencies: + pify "^3.0.0" + +make-error@^1.1.1: + version "1.3.5" + resolved "http://registry.npm.taobao.org/make-error/download/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + +map-cache@^0.2.2: + version "0.2.2" + resolved "http://registry.npm.taobao.org/map-cache/download/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/map-obj/download/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +map-obj@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/map-obj/download/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" + +map-stream@0.0.7: + version "0.0.7" + resolved "http://registry.npm.taobao.org/map-stream/download/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" + +map-visit@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/map-visit/download/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + dependencies: + object-visit "^1.0.0" + +marked@^0.5.0: + version "0.5.0" + resolved "http://registry.npm.taobao.org/marked/download/marked-0.5.0.tgz#9e590bad31584a48ff405b33ab1c0dd25172288e" + +matcher@^1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/matcher/download/matcher-1.1.1.tgz#51d8301e138f840982b338b116bb0c09af62c1c2" + dependencies: + escape-string-regexp "^1.0.4" + +md5-hex@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/md5-hex/download/md5-hex-2.0.0.tgz#d0588e9f1c74954492ecd24ac0ac6ce997d92e33" + dependencies: + md5-o-matic "^0.1.1" + +md5-o-matic@^0.1.1: + version "0.1.1" + resolved "http://registry.npm.taobao.org/md5-o-matic/download/md5-o-matic-0.1.1.tgz#822bccd65e117c514fab176b25945d54100a03c3" + +md5@^2.2.1: + version "2.2.1" + resolved "http://registry.npm.taobao.org/md5/download/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + +media-typer@0.3.0: + version "0.3.0" + resolved "http://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +mem@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/mem/download/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +memory-pager@^1.0.2: + version "1.1.0" + resolved "http://registry.npm.taobao.org/memory-pager/download/memory-pager-1.1.0.tgz#9308915e0e972849fefbae6f8bc95d6b350e7344" + +meow@^3.3.0: + version "3.7.0" + resolved "http://registry.npm.taobao.org/meow/download/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +meow@^4.0.0: + version "4.0.1" + resolved "http://registry.npm.taobao.org/meow/download/meow-4.0.1.tgz#d48598f6f4b1472f35bf6317a95945ace347f975" + dependencies: + camelcase-keys "^4.0.0" + decamelize-keys "^1.0.0" + loud-rejection "^1.0.0" + minimist "^1.1.3" + minimist-options "^3.0.1" + normalize-package-data "^2.3.4" + read-pkg-up "^3.0.0" + redent "^2.0.0" + trim-newlines "^2.0.0" + +merge-descriptors@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/merge-descriptors/download/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +merge-estraverse-visitors@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/merge-estraverse-visitors/download/merge-estraverse-visitors-1.0.0.tgz#eb968338b5ded5ceed82cec0307decba2d8ea994" + dependencies: + estraverse "^4.0.0" + +merge-source-map@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/merge-source-map/download/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + dependencies: + source-map "^0.6.1" + +merge2@^1.2.1: + version "1.2.2" + resolved "http://registry.npm.taobao.org/merge2/download/merge2-1.2.2.tgz#03212e3da8d86c4d8523cebd6318193414f94e34" + +merge@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/merge/download/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + +methods@^1.0.1, methods@^1.1.1, methods@^1.1.2: + version "1.1.2" + resolved "http://registry.npm.taobao.org/methods/download/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "http://registry.npm.taobao.org/micromatch/download/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +"mime-db@>= 1.36.0 < 2", mime-db@~1.36.0: + version "1.36.0" + resolved "http://registry.npm.taobao.org/mime-db/download/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" + +mime-types@2.1.20, mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.8, mime-types@~2.1.18: + version "2.1.20" + resolved "http://registry.npm.taobao.org/mime-types/download/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" + dependencies: + mime-db "~1.36.0" + +mime@^1.3.4, mime@^1.4.1: + version "1.6.0" + resolved "http://registry.npm.taobao.org/mime/download/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/mimic-fn/download/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + +mimic-response@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/mimic-response/download/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + +mingo@^1.3.1: + version "1.3.3" + resolved "http://registry.npm.taobao.org/mingo/download/mingo-1.3.3.tgz#6922c4d147efc771a01425a2c4c8f7784478c546" + +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: + version "3.0.4" + resolved "http://registry.npm.taobao.org/minimatch/download/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist-options@^3.0.1: + version "3.0.2" + resolved "http://registry.npm.taobao.org/minimist-options/download/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + +minimist@0.0.8: + version "0.0.8" + resolved "http://registry.npm.taobao.org/minimist/download/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/minimist/download/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +minimist@~0.0.1: + version "0.0.10" + resolved "http://registry.npm.taobao.org/minimist/download/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + +minipass@^2.2.1, minipass@^2.3.3: + version "2.3.4" + resolved "http://registry.npm.taobao.org/minipass/download/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957" + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/minizlib/download/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" + dependencies: + minipass "^2.2.1" + +mixin-deep@^1.2.0: + version "1.3.1" + resolved "http://registry.npm.taobao.org/mixin-deep/download/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "http://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mm@^2.4.1: + version "2.4.1" + resolved "http://registry.npm.taobao.org/mm/download/mm-2.4.1.tgz#d5885ecb954196e89393cff255b84782feb3a1bc" + dependencies: + is-type-of "^1.0.0" + ko-sleep "^1.0.2" + muk-prop "^1.0.0" + thenify "^3.2.1" + +mocha@^5.2.0: + version "5.2.0" + resolved "http://registry.npm.taobao.org/mocha/download/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + +modify-values@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/modify-values/download/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" + +moment-timezone@^0.5.0: + version "0.5.21" + resolved "http://registry.npm.taobao.org/moment-timezone/download/moment-timezone-0.5.21.tgz#3cba247d84492174dbf71de2a9848fa13207b845" + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.19.2, moment@^2.19.3, moment@^2.22.2: + version "2.22.2" + resolved "http://registry.npm.taobao.org/moment/download/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + +mongodb-core@3.1.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/mongodb-core/download/mongodb-core-3.1.0.tgz#af91f36fd560ed785f4e61e694432df4d3698aad" + dependencies: + bson "~1.0.4" + require_optional "^1.0.1" + optionalDependencies: + saslprep "^1.0.0" + +mongodb-core@3.1.3: + version "3.1.3" + resolved "http://registry.npm.taobao.org/mongodb-core/download/mongodb-core-3.1.3.tgz#b036bce5290b383fe507238965bef748dd8adb75" + dependencies: + bson "^1.1.0" + require_optional "^1.0.1" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + +mongodb@3.1.1: + version "3.1.1" + resolved "http://registry.npm.taobao.org/mongodb/download/mongodb-3.1.1.tgz#c018c4b277614e8b1e08426d5bcbe1a7e5cdbd74" + dependencies: + mongodb-core "3.1.0" + +mongodb@3.1.4: + version "3.1.4" + resolved "http://registry.npm.taobao.org/mongodb/download/mongodb-3.1.4.tgz#0ff07a7409a4edf05e71f9ff8df3633bd278ed53" + dependencies: + mongodb-core "3.1.3" + safe-buffer "^5.1.2" + +mongoose-legacy-pluralize@1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/mongoose-legacy-pluralize/download/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" + +mongoose-paginate-v2@^1.0.12: + version "1.0.12" + resolved "http://registry.npm.taobao.org/mongoose-paginate-v2/download/mongoose-paginate-v2-1.0.12.tgz#b7c502fc816dcf6785acb6f086f4ec97616163c4" + dependencies: + bluebird "3.5.1" + +mongoose@5.2.8: + version "5.2.8" + resolved "http://registry.npm.taobao.org/mongoose/download/mongoose-5.2.8.tgz#dd74ce0c4df803cb816c37ee1228d6663e2c2254" + dependencies: + async "2.6.1" + bson "~1.0.5" + kareem "2.2.1" + lodash.get "4.4.2" + mongodb "3.1.1" + mongodb-core "3.1.0" + mongoose-legacy-pluralize "1.0.2" + mpath "0.4.1" + mquery "3.1.2" + ms "2.0.0" + regexp-clone "0.0.1" + safe-buffer "5.1.2" + sliced "1.0.1" + +mongoose@^5.0.17: + version "5.2.15" + resolved "http://registry.npm.taobao.org/mongoose/download/mongoose-5.2.15.tgz#3e6459bd7a074f9cbc142052f4d56eae0097adea" + dependencies: + async "2.6.1" + bson "~1.0.5" + kareem "2.2.1" + lodash.get "4.4.2" + mongodb "3.1.4" + mongodb-core "3.1.3" + mongoose-legacy-pluralize "1.0.2" + mpath "0.5.1" + mquery "3.2.0" + ms "2.0.0" + regexp-clone "0.0.1" + safe-buffer "5.1.2" + sliced "1.0.1" + +mpath@0.4.1: + version "0.4.1" + resolved "http://registry.npm.taobao.org/mpath/download/mpath-0.4.1.tgz#ed10388430380bf7bbb5be1391e5d6969cb08e89" + +mpath@0.5.1: + version "0.5.1" + resolved "http://registry.npm.taobao.org/mpath/download/mpath-0.5.1.tgz#17131501f1ff9e6e4fbc8ffa875aa7065b5775ab" + +mquery@3.1.2: + version "3.1.2" + resolved "http://registry.npm.taobao.org/mquery/download/mquery-3.1.2.tgz#46c2ea6d7a08c9b9e0716022fb2990708ddba9ff" + dependencies: + bluebird "3.5.1" + debug "3.1.0" + regexp-clone "0.0.1" + sliced "1.0.1" + +mquery@3.2.0: + version "3.2.0" + resolved "http://registry.npm.taobao.org/mquery/download/mquery-3.2.0.tgz#e276472abd5109686a15eb2a8e0761db813c81cc" + dependencies: + bluebird "3.5.1" + debug "3.1.0" + regexp-clone "0.0.1" + safe-buffer "5.1.2" + sliced "1.0.1" + +ms@2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +ms@^2.0.0, ms@^2.1.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/ms/download/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + +muk-prop@^1.0.0: + version "1.2.1" + resolved "http://registry.npm.taobao.org/muk-prop/download/muk-prop-1.2.1.tgz#40fa3d6e93553b2016a9fb77d8918568c57ae14d" + +multi-stage-sourcemap@^0.2.1: + version "0.2.1" + resolved "http://registry.npm.taobao.org/multi-stage-sourcemap/download/multi-stage-sourcemap-0.2.1.tgz#b09fc8586eaa17f81d575c4ad02e0f7a3f6b1105" + dependencies: + source-map "^0.1.34" + +multimatch@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/multimatch/download/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" + dependencies: + array-differ "^1.0.0" + array-union "^1.0.1" + arrify "^1.0.0" + minimatch "^3.0.0" + +mustache@^2.3.0: + version "2.3.2" + resolved "http://registry.npm.taobao.org/mustache/download/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + +mute-stream@0.0.7: + version "0.0.7" + resolved "http://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + +mz-modules@^2.0.0, mz-modules@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/mz-modules/download/mz-modules-2.1.0.tgz#7f529877afd0d42f409a7463b96986d61cfbcf96" + dependencies: + glob "^7.1.2" + ko-sleep "^1.0.3" + mkdirp "^0.5.1" + pump "^3.0.0" + rimraf "^2.6.1" + +mz@^2.6.0, mz@^2.7.0: + version "2.7.0" + resolved "http://registry.npm.taobao.org/mz/download/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nan@^2.9.2: + version "2.11.0" + resolved "http://registry.npm.taobao.org/nan/download/nan-2.11.0.tgz#574e360e4d954ab16966ec102c0c049fd961a099" + +nanoid@^1.1.1: + version "1.2.3" + resolved "http://registry.npm.taobao.org/nanoid/download/nanoid-1.2.3.tgz#b8f022193a5808f9e3112658a2c34e43b24c009a" + +nanomatch@^1.2.9: + version "1.2.13" + resolved "http://registry.npm.taobao.org/nanomatch/download/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "http://registry.npm.taobao.org/natural-compare/download/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +ndir@^0.1.5: + version "0.1.5" + resolved "http://registry.npm.taobao.org/ndir/download/ndir-0.1.5.tgz#120891d7697bbbe8214cfeff09602060d3454558" + +needle@^2.2.1: + version "2.2.3" + resolved "http://registry.npm.taobao.org/needle/download/needle-2.2.3.tgz#c1b04da378cd634d8befe2de965dc2cfb0fd65ca" + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" + +negotiator@0.6.1: + version "0.6.1" + resolved "http://registry.npm.taobao.org/negotiator/download/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +nested-error-stacks@^2.0.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/nested-error-stacks/download/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" + +netmask@^1.0.6: + version "1.0.6" + resolved "http://registry.npm.taobao.org/netmask/download/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" + +next-tick@1: + version "1.0.0" + resolved "http://registry.npm.taobao.org/next-tick/download/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + +nice-try@^1.0.4: + version "1.0.5" + resolved "http://registry.npm.taobao.org/nice-try/download/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + +no-case@^2.2.0, no-case@^2.3.2: + version "2.3.2" + resolved "http://registry.npm.taobao.org/no-case/download/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + dependencies: + lower-case "^1.1.1" + +node-fetch@^2.1.1: + version "2.2.0" + resolved "http://registry.npm.taobao.org/node-fetch/download/node-fetch-2.2.0.tgz#4ee79bde909262f9775f731e3656d0db55ced5b5" + +node-homedir@^1.1.0, node-homedir@^1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/node-homedir/download/node-homedir-1.1.1.tgz#736db0b60e3bba8aba68df9927de40a7aabe1075" + +node-pre-gyp@^0.10.0: + version "0.10.3" + resolved "http://registry.npm.taobao.org/node-pre-gyp/download/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +nodemailer@^4.6.8: + version "4.6.8" + resolved "http://registry.npm.taobao.org/nodemailer/download/nodemailer-4.6.8.tgz#f82fb407828bf2e76d92acc34b823d83e774f89c" + +nopt@^4.0.1: + version "4.0.1" + resolved "http://registry.npm.taobao.org/nopt/download/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5: + version "2.4.0" + resolved "http://registry.npm.taobao.org/normalize-package-data/download/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/normalize-path/download/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-url@2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/normalize-url/download/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" + dependencies: + prepend-http "^2.0.0" + query-string "^5.0.1" + sort-keys "^2.0.0" + +nounou@^1.2.1: + version "1.2.1" + resolved "http://registry.npm.taobao.org/nounou/download/nounou-1.2.1.tgz#a9047efa4405e26ec7e768875bd2580fc4a5f08f" + +npm-bundled@^1.0.1: + version "1.0.5" + resolved "http://registry.npm.taobao.org/npm-bundled/download/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" + +npm-packlist@^1.1.6: + version "1.1.11" + resolved "http://registry.npm.taobao.org/npm-packlist/download/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de" + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "http://registry.npm.taobao.org/npm-run-path/download/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +npmlog@^4.0.2: + version "4.1.2" + resolved "http://registry.npm.taobao.org/npmlog/download/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/number-is-nan/download/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +nunjucks@^3.0.1: + version "3.1.3" + resolved "http://registry.npm.taobao.org/nunjucks/download/nunjucks-3.1.3.tgz#9a23c844af01c143a0b40f3bdd1212a9d7877260" + dependencies: + a-sync-waterfall "^1.0.0" + asap "^2.0.3" + postinstall-build "^5.0.1" + yargs "^3.32.0" + optionalDependencies: + chokidar "^2.0.0" + +nyc@^13.0.1: + version "13.0.1" + resolved "http://registry.npm.taobao.org/nyc/download/nyc-13.0.1.tgz#b61857ed633c803353fc41eeca775d0e1f62034b" + dependencies: + archy "^1.0.0" + arrify "^1.0.1" + caching-transform "^2.0.0" + convert-source-map "^1.5.1" + debug-log "^1.0.1" + find-cache-dir "^2.0.0" + find-up "^3.0.0" + foreground-child "^1.5.6" + glob "^7.1.2" + istanbul-lib-coverage "^2.0.1" + istanbul-lib-hook "^2.0.1" + istanbul-lib-instrument "^2.3.2" + istanbul-lib-report "^2.0.1" + istanbul-lib-source-maps "^2.0.1" + istanbul-reports "^2.0.0" + make-dir "^1.3.0" + merge-source-map "^1.1.0" + resolve-from "^4.0.0" + rimraf "^2.6.2" + signal-exit "^3.0.2" + spawn-wrap "^1.4.2" + test-exclude "^5.0.0" + uuid "^3.3.2" + yargs "11.1.0" + yargs-parser "^9.0.2" + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "http://registry.npm.taobao.org/object-assign/download/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-copy@^0.1.0: + version "0.1.0" + resolved "http://registry.npm.taobao.org/object-copy/download/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-keys@^1.0.0, object-keys@^1.0.12: + version "1.0.12" + resolved "http://registry.npm.taobao.org/object-keys/download/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" + +object-visit@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/object-visit/download/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + dependencies: + isobject "^3.0.0" + +object.pick@^1.3.0: + version "1.3.0" + resolved "http://registry.npm.taobao.org/object.pick/download/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + dependencies: + isobject "^3.0.1" + +on-finished@^2.3.0: + version "2.3.0" + resolved "http://registry.npm.taobao.org/on-finished/download/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "http://registry.npm.taobao.org/once/download/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "http://registry.npm.taobao.org/onetime/download/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + +only@~0.0.2: + version "0.0.2" + resolved "http://registry.npm.taobao.org/only/download/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" + +optimist@^0.6.1: + version "0.6.1" + resolved "http://registry.npm.taobao.org/optimist/download/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.1, optionator@^0.8.2: + version "0.8.2" + resolved "http://registry.npm.taobao.org/optionator/download/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +options@>=0.0.5: + version "0.0.6" + resolved "http://registry.npm.taobao.org/options/download/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" + +ora@3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/ora/download/ora-3.0.0.tgz#8179e3525b9aafd99242d63cc206fd64732741d0" + dependencies: + chalk "^2.3.1" + cli-cursor "^2.1.0" + cli-spinners "^1.1.0" + log-symbols "^2.2.0" + strip-ansi "^4.0.0" + wcwidth "^1.0.1" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "http://registry.npm.taobao.org/os-homedir/download/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "http://registry.npm.taobao.org/os-locale/download/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-locale@^2.0.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/os-locale/download/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-name@2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/os-name/download/os-name-2.0.1.tgz#b9a386361c17ae3a21736ef0599405c9a8c5dc5e" + dependencies: + macos-release "^1.0.0" + win-release "^1.0.0" + +os-name@~1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/os-name/download/os-name-1.0.3.tgz#1b379f64835af7c5a7f498b357cb95215c159edf" + dependencies: + osx-release "^1.0.0" + win-release "^1.0.0" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/os-tmpdir/download/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.4: + version "0.1.5" + resolved "http://registry.npm.taobao.org/osenv/download/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +osx-release@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/osx-release/download/osx-release-1.1.0.tgz#f217911a28136949af1bf9308b241e2737d3cd6c" + dependencies: + minimist "^1.1.0" + +p-cancelable@^0.4.0: + version "0.4.1" + resolved "http://registry.npm.taobao.org/p-cancelable/download/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/p-finally/download/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-is-promise@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/p-is-promise/download/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" + +p-limit@^1.1.0: + version "1.3.0" + resolved "http://registry.npm.taobao.org/p-limit/download/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/p-limit/download/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec" + dependencies: + p-try "^2.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/p-locate/download/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/p-locate/download/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + dependencies: + p-limit "^2.0.0" + +p-timeout@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/p-timeout/download/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" + dependencies: + p-finally "^1.0.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/p-try/download/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +p-try@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/p-try/download/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" + +pac-proxy-agent@^2.0.1: + version "2.0.2" + resolved "http://registry.npm.taobao.org/pac-proxy-agent/download/pac-proxy-agent-2.0.2.tgz#90d9f6730ab0f4d2607dcdcd4d3d641aa26c3896" + dependencies: + agent-base "^4.2.0" + debug "^3.1.0" + get-uri "^2.0.0" + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.1" + pac-resolver "^3.0.0" + raw-body "^2.2.0" + socks-proxy-agent "^3.0.0" + +pac-resolver@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/pac-resolver/download/pac-resolver-3.0.0.tgz#6aea30787db0a891704deb7800a722a7615a6f26" + dependencies: + co "^4.6.0" + degenerator "^1.0.4" + ip "^1.1.5" + netmask "^1.0.6" + thunkify "^2.1.2" + +package-hash@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/package-hash/download/package-hash-2.0.0.tgz#78ae326c89e05a4d813b68601977af05c00d2a0d" + dependencies: + graceful-fs "^4.1.11" + lodash.flattendeep "^4.4.0" + md5-hex "^2.0.0" + release-zalgo "^1.0.0" + +package-json@^4.0.0: + version "4.0.1" + resolved "http://registry.npm.taobao.org/package-json/download/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" + dependencies: + got "^6.7.1" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + +param-case@^2.1.0: + version "2.1.1" + resolved "http://registry.npm.taobao.org/param-case/download/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" + dependencies: + no-case "^2.2.0" + +parameter@^2.4.0: + version "2.4.0" + resolved "http://registry.npm.taobao.org/parameter/download/parameter-2.4.0.tgz#85ea52e9dfee80265a07b086edbc352e68b39733" + +parse-github-repo-url@^1.3.0: + version "1.4.1" + resolved "http://registry.npm.taobao.org/parse-github-repo-url/download/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" + +parse-json@^2.2.0: + version "2.2.0" + resolved "http://registry.npm.taobao.org/parse-json/download/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/parse-json/download/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-repo@1.0.4: + version "1.0.4" + resolved "http://registry.npm.taobao.org/parse-repo/download/parse-repo-1.0.4.tgz#74b91d2cb8675d11b99976a0065f6ce17fa1bcc8" + +parseurl@^1.3.2: + version "1.3.2" + resolved "http://registry.npm.taobao.org/parseurl/download/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + +pascal-case@^2.0.0: + version "2.0.1" + resolved "http://registry.npm.taobao.org/pascal-case/download/pascal-case-2.0.1.tgz#2d578d3455f660da65eca18ef95b4e0de912761e" + dependencies: + camel-case "^3.0.0" + upper-case-first "^1.1.0" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "http://registry.npm.taobao.org/pascalcase/download/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + +path-case@^2.1.0: + version "2.1.1" + resolved "http://registry.npm.taobao.org/path-case/download/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5" + dependencies: + no-case "^2.2.0" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "http://registry.npm.taobao.org/path-dirname/download/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + +path-exists@^2.0.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/path-exists/download/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/path-exists/download/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/path-is-absolute/download/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1, path-is-inside@^1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/path-is-inside/download/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/path-key/download/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.6" + resolved "http://registry.npm.taobao.org/path-parse/download/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + +path-to-regexp@^1.1.1, path-to-regexp@^1.7.0: + version "1.7.0" + resolved "http://registry.npm.taobao.org/path-to-regexp/download/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + +path-type@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/path-type/download/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/path-type/download/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +path-type@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/path-type/download/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + dependencies: + pify "^3.0.0" + +pause-stream@^0.0.11: + version "0.0.11" + resolved "http://registry.npm.taobao.org/pause-stream/download/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + dependencies: + through "~2.3" + +pedding@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/pedding/download/pedding-1.1.0.tgz#f7b138c288d4bd584eada1215f5bd924f1e1e667" + +pend@~1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/pend/download/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "http://registry.npm.taobao.org/pify/download/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/pify/download/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "http://registry.npm.taobao.org/pinkie-promise/download/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "http://registry.npm.taobao.org/pinkie/download/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pkg-dir@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/pkg-dir/download/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" + dependencies: + find-up "^1.0.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/pkg-dir/download/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + dependencies: + find-up "^3.0.0" + +platform@^1.3.1, platform@^1.3.4: + version "1.3.5" + resolved "http://registry.npm.taobao.org/platform/download/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444" + +pluralize@^7.0.0: + version "7.0.0" + resolved "http://registry.npm.taobao.org/pluralize/download/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "http://registry.npm.taobao.org/posix-character-classes/download/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + +postinstall-build@^5.0.1: + version "5.0.3" + resolved "http://registry.npm.taobao.org/postinstall-build/download/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" + +power-assert-context-formatter@^1.0.7: + version "1.2.0" + resolved "http://registry.npm.taobao.org/power-assert-context-formatter/download/power-assert-context-formatter-1.2.0.tgz#8fbe72692288ec5a7203cdf215c8b838a6061d2a" + dependencies: + core-js "^2.0.0" + power-assert-context-traversal "^1.2.0" + +power-assert-context-reducer-ast@^1.0.7: + version "1.2.0" + resolved "http://registry.npm.taobao.org/power-assert-context-reducer-ast/download/power-assert-context-reducer-ast-1.2.0.tgz#c7ca1c9e39a6fb717f7ac5fe9e76e192bf525df3" + dependencies: + acorn "^5.0.0" + acorn-es7-plugin "^1.0.12" + core-js "^2.0.0" + espurify "^1.6.0" + estraverse "^4.2.0" + +power-assert-context-traversal@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/power-assert-context-traversal/download/power-assert-context-traversal-1.2.0.tgz#f6e71454baf640de5c1c9c270349f5c9ab0b2e94" + dependencies: + core-js "^2.0.0" + estraverse "^4.1.0" + +power-assert-formatter@^1.4.1: + version "1.4.1" + resolved "http://registry.npm.taobao.org/power-assert-formatter/download/power-assert-formatter-1.4.1.tgz#5dc125ed50a3dfb1dda26c19347f3bf58ec2884a" + dependencies: + core-js "^2.0.0" + power-assert-context-formatter "^1.0.7" + power-assert-context-reducer-ast "^1.0.7" + power-assert-renderer-assertion "^1.0.7" + power-assert-renderer-comparison "^1.0.7" + power-assert-renderer-diagram "^1.0.7" + power-assert-renderer-file "^1.0.7" + +power-assert-renderer-assertion@^1.0.7: + version "1.2.0" + resolved "http://registry.npm.taobao.org/power-assert-renderer-assertion/download/power-assert-renderer-assertion-1.2.0.tgz#3db6ffcda106b37bc1e06432ad0d748a682b147a" + dependencies: + power-assert-renderer-base "^1.1.1" + power-assert-util-string-width "^1.2.0" + +power-assert-renderer-base@^1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/power-assert-renderer-base/download/power-assert-renderer-base-1.1.1.tgz#96a650c6fd05ee1bc1f66b54ad61442c8b3f63eb" + +power-assert-renderer-comparison@^1.0.7: + version "1.2.0" + resolved "http://registry.npm.taobao.org/power-assert-renderer-comparison/download/power-assert-renderer-comparison-1.2.0.tgz#e4f88113225a69be8aa586ead05aef99462c0495" + dependencies: + core-js "^2.0.0" + diff-match-patch "^1.0.0" + power-assert-renderer-base "^1.1.1" + stringifier "^1.3.0" + type-name "^2.0.1" + +power-assert-renderer-diagram@^1.0.7: + version "1.2.0" + resolved "http://registry.npm.taobao.org/power-assert-renderer-diagram/download/power-assert-renderer-diagram-1.2.0.tgz#37f66e8542e5677c5b58e6d72b01c0d9a30e2219" + dependencies: + core-js "^2.0.0" + power-assert-renderer-base "^1.1.1" + power-assert-util-string-width "^1.2.0" + stringifier "^1.3.0" + +power-assert-renderer-file@^1.0.7: + version "1.2.0" + resolved "http://registry.npm.taobao.org/power-assert-renderer-file/download/power-assert-renderer-file-1.2.0.tgz#3f4bebd9e1455d75cf2ac541e7bb515a87d4ce4b" + dependencies: + power-assert-renderer-base "^1.1.1" + +power-assert-util-string-width@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/power-assert-util-string-width/download/power-assert-util-string-width-1.2.0.tgz#6e06d5e3581bb876c5d377c53109fffa95bd91a0" + dependencies: + eastasianwidth "^0.2.0" + +power-assert@^1.6.0, power-assert@^1.6.1: + version "1.6.1" + resolved "http://registry.npm.taobao.org/power-assert/download/power-assert-1.6.1.tgz#b28cbc02ae808afd1431d0cd5093a39ac5a5b1fe" + dependencies: + define-properties "^1.1.2" + empower "^1.3.1" + power-assert-formatter "^1.4.1" + universal-deep-strict-equal "^1.2.1" + xtend "^4.0.0" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "http://registry.npm.taobao.org/prelude-ls/download/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +prepend-http@^1.0.1: + version "1.0.4" + resolved "http://registry.npm.taobao.org/prepend-http/download/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +prepend-http@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/prepend-http/download/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + +prettier@^1.13.7: + version "1.14.3" + resolved "http://registry.npm.taobao.org/prettier/download/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895" + +printable@^0.0.3: + version "0.0.3" + resolved "http://registry.npm.taobao.org/printable/download/printable-0.0.3.tgz#f653cb39b214b78049ae1403e2fb05d74a6d50e0" + +private@^0.1.6, private@^0.1.8: + version "0.1.8" + resolved "http://registry.npm.taobao.org/private/download/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/process-nextick-args/download/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +progress@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/progress/download/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + +prop-types@^15.6.2: + version "15.6.2" + resolved "http://registry.npm.taobao.org/prop-types/download/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" + dependencies: + loose-envify "^1.3.1" + object-assign "^4.1.1" + +proxy-agent@^2.1.0: + version "2.3.1" + resolved "http://registry.npm.taobao.org/proxy-agent/download/proxy-agent-2.3.1.tgz#3d49d863d46cf5f37ca8394848346ea02373eac6" + dependencies: + agent-base "^4.2.0" + debug "^3.1.0" + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.1" + lru-cache "^4.1.2" + pac-proxy-agent "^2.0.1" + proxy-from-env "^1.0.0" + socks-proxy-agent "^3.0.0" + +proxy-from-env@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/proxy-from-env/download/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/pseudomap/download/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +pump@^2.0.0: + version "2.0.1" + resolved "http://registry.npm.taobao.org/pump/download/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/pump/download/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.5: + version "1.5.1" + resolved "http://registry.npm.taobao.org/pumpify/download/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +q@^1.4.1, q@^1.5.1: + version "1.5.1" + resolved "http://registry.npm.taobao.org/q/download/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + +qs@^6.4.0, qs@^6.5.1, qs@^6.5.2: + version "6.5.2" + resolved "http://registry.npm.taobao.org/qs/download/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + +query-string@^5.0.1: + version "5.1.1" + resolved "http://registry.npm.taobao.org/query-string/download/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring@0.2.0: + version "0.2.0" + resolved "http://registry.npm.taobao.org/querystring/download/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +quick-lru@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/quick-lru/download/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" + +random-bytes@~1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/random-bytes/download/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + +raven@^2.2.1: + version "2.6.4" + resolved "http://registry.npm.taobao.org/raven/download/raven-2.6.4.tgz#458d4a380c8fbb59e0150c655625aaf60c167ea3" + dependencies: + cookie "0.3.1" + md5 "^2.2.1" + stack-trace "0.0.10" + timed-out "4.0.1" + uuid "3.3.2" + +raw-body@^2.2.0, raw-body@^2.3.3: + version "2.3.3" + resolved "http://registry.npm.taobao.org/raw-body/download/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + +rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: + version "1.2.8" + resolved "http://registry.npm.taobao.org/rc/download/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/read-pkg-up/download/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/read-pkg-up/download/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/read-pkg-up/download/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" + dependencies: + find-up "^2.0.0" + read-pkg "^3.0.0" + +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/read-pkg-up/download/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + +read-pkg@^1.0.0, read-pkg@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/read-pkg/download/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/read-pkg/download/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/read-pkg/download/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +readable-stream@1.1.x: + version "1.1.14" + resolved "http://registry.npm.taobao.org/readable-stream/download/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.5, readable-stream@^2.3.6: + version "2.3.6" + resolved "http://registry.npm.taobao.org/readable-stream/download/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.2.1" + resolved "http://registry.npm.taobao.org/readdirp/download/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +ready-callback@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/ready-callback/download/ready-callback-2.1.0.tgz#e382a9e33a568b8d771e04ef4ef0eb02d3dfa7e0" + dependencies: + debug "^2.6.0" + get-ready "^2.0.0" + once "^1.4.0" + uuid "^3.0.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "http://registry.npm.taobao.org/rechoir/download/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +redent@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/redent/download/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +redent@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/redent/download/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" + dependencies: + indent-string "^3.0.0" + strip-indent "^2.0.0" + +redis-commands@^1.2.0: + version "1.3.5" + resolved "http://registry.npm.taobao.org/redis-commands/download/redis-commands-1.3.5.tgz#4495889414f1e886261180b1442e7295602d83a2" + +redis-parser@^2.4.0: + version "2.6.0" + resolved "http://registry.npm.taobao.org/redis-parser/download/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" + +regenerate@^1.2.1: + version "1.4.0" + resolved "http://registry.npm.taobao.org/regenerate/download/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "http://registry.npm.taobao.org/regenerator-runtime/download/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + +regenerator-transform@^0.10.0: + version "0.10.1" + resolved "http://registry.npm.taobao.org/regenerator-transform/download/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/regex-not/download/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp-clone@0.0.1: + version "0.0.1" + resolved "http://registry.npm.taobao.org/regexp-clone/download/regexp-clone-0.0.1.tgz#a7c2e09891fdbf38fbb10d376fb73003e68ac589" + +regexpp@^1.0.1: + version "1.1.0" + resolved "http://registry.npm.taobao.org/regexpp/download/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/regexpu-core/download/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +registry-auth-token@^3.0.1: + version "3.3.2" + resolved "http://registry.npm.taobao.org/registry-auth-token/download/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20" + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@^3.0.3: + version "3.1.0" + resolved "http://registry.npm.taobao.org/registry-url/download/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + dependencies: + rc "^1.0.1" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "http://registry.npm.taobao.org/regjsgen/download/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "http://registry.npm.taobao.org/regjsparser/download/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +release-it@^7.6.1: + version "7.6.1" + resolved "http://registry.npm.taobao.org/release-it/download/release-it-7.6.1.tgz#3b9d509da36a214deba3ff1299f43f84c9e591ba" + dependencies: + "@octokit/rest" "15.10.0" + async-retry "1.2.1" + babel-preset-env "1.7.0" + babel-register "6.26.0" + bump-file "1.0.0" + chalk "2.4.1" + conventional-changelog "2.0.3" + conventional-recommended-bump "4.0.1" + cpy "7.0.1" + debug "3.1.0" + globby "8.0.1" + got "8.3.2" + inquirer "6.2.0" + is-ci "1.2.0" + lodash "4.17.10" + mime-types "2.1.20" + ora "3.0.0" + os-name "2.0.1" + parse-repo "1.0.4" + semver "5.5.1" + shelljs "0.8.2" + supports-color "5.5.0" + tmp-promise "1.0.5" + update-notifier "2.5.0" + uuid "3.3.2" + window-size "1.1.1" + yargs-parser "10.1.0" + +release-zalgo@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/release-zalgo/download/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" + dependencies: + es6-error "^4.0.1" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "http://registry.npm.taobao.org/remove-trailing-separator/download/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.3" + resolved "http://registry.npm.taobao.org/repeat-element/download/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + +repeat-string@^1.6.1: + version "1.6.1" + resolved "http://registry.npm.taobao.org/repeat-string/download/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "http://registry.npm.taobao.org/repeating/download/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/require-directory/download/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/require-main-filename/download/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +require-uncached@^1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/require-uncached/download/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +require_optional@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/require_optional/download/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" + dependencies: + resolve-from "^2.0.0" + semver "^5.1.0" + +resolve-files@^1.0.0: + version "1.0.2" + resolved "http://registry.npm.taobao.org/resolve-files/download/resolve-files-1.0.2.tgz#5a72118b92fa7394ff2d8605d1fc373e9c3e90ef" + dependencies: + crequire "^1.8.0" + debug "^2.6.3" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/resolve-from/download/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve-from@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/resolve-from/download/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/resolve-from/download/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "http://registry.npm.taobao.org/resolve-url/download/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + +resolve@^1.1.6, resolve@^1.5.0, resolve@^1.6.0: + version "1.8.1" + resolved "http://registry.npm.taobao.org/resolve/download/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" + dependencies: + path-parse "^1.0.5" + +responselike@1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/responselike/download/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + dependencies: + lowercase-keys "^1.0.0" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/restore-cursor/download/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "http://registry.npm.taobao.org/ret/download/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + +retry@0.10.1: + version "0.10.1" + resolved "http://registry.npm.taobao.org/retry/download/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" + +rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.6.1, rimraf@^2.6.2: + version "2.6.2" + resolved "http://registry.npm.taobao.org/rimraf/download/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +rndm@1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/rndm/download/rndm-1.2.0.tgz#f33fe9cfb52bbfd520aa18323bc65db110a1b76c" + +run-async@^2.2.0: + version "2.3.0" + resolved "http://registry.npm.taobao.org/run-async/download/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +runscript@^1.3.0: + version "1.3.0" + resolved "http://registry.npm.taobao.org/runscript/download/runscript-1.3.0.tgz#291e77cbe23580c00ea7c0eb1d208fac86aa1306" + dependencies: + debug "^2.6.8" + is-type-of "^1.1.0" + +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "http://registry.npm.taobao.org/rx-lite-aggregates/download/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "http://registry.npm.taobao.org/rx-lite/download/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" + +rxjs@^6.1.0: + version "6.3.2" + resolved "http://registry.npm.taobao.org/rxjs/download/rxjs-6.3.2.tgz#6a688b16c4e6e980e62ea805ec30648e1c60907f" + dependencies: + tslib "^1.9.0" + +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "http://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +safe-regex@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/safe-regex/download/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + dependencies: + ret "~0.1.10" + +safe-timers@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/safe-timers/download/safe-timers-1.1.0.tgz#c58ae8325db8d3b067322f0a4ef3a0cad67aad83" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "http://registry.npm.taobao.org/safer-buffer/download/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +saslprep@^1.0.0: + version "1.0.2" + resolved "http://registry.npm.taobao.org/saslprep/download/saslprep-1.0.2.tgz#da5ab936e6ea0bbae911ffec77534be370c9f52d" + dependencies: + sparse-bitfield "^3.0.3" + +save@^2.3.2: + version "2.3.2" + resolved "http://registry.npm.taobao.org/save/download/save-2.3.2.tgz#8592674b5565cc4e12bc6ddd9cb09f828e3ecf7d" + dependencies: + async "^2.4.1" + event-stream "^3.3.4" + lodash.assign "^4.2.0" + mingo "^1.3.1" + +sax@=0.4.2: + version "0.4.2" + resolved "http://registry.npm.taobao.org/sax/download/sax-0.4.2.tgz#39f3b601733d6bec97105b242a2a40fd6978ac3c" + +sax@>=0.6.0, sax@^1.2.4: + version "1.2.4" + resolved "http://registry.npm.taobao.org/sax/download/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +scmp@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/scmp/download/scmp-2.0.0.tgz#247110ef22ccf897b13a3f0abddb52782393cd6a" + +sdk-base@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/sdk-base/download/sdk-base-2.0.1.tgz#ba40289e8bdf272ed11dd9ea97eaf98e036d24c6" + dependencies: + get-ready "~1.0.0" + +sdk-base@^3.1.1, sdk-base@^3.3.0, sdk-base@^3.4.0: + version "3.5.0" + resolved "http://registry.npm.taobao.org/sdk-base/download/sdk-base-3.5.0.tgz#15111f3a8ce0968bbbddd0ab27ca60ae4b591da2" + dependencies: + await-event "^2.1.0" + await-first "^1.0.0" + co "^4.6.0" + is-type-of "^1.2.0" + +semver-diff@^2.0.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/semver-diff/download/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + dependencies: + semver "^5.0.3" + +"semver@2 || 3 || 4 || 5", semver@5.5.1, semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: + version "5.5.1" + resolved "http://registry.npm.taobao.org/semver/download/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" + +semver@5.4.1: + version "5.4.1" + resolved "http://registry.npm.taobao.org/semver/download/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +sendmessage@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/sendmessage/download/sendmessage-1.1.0.tgz#10a245cee2d50c759f1e09a23477b91496d09e35" + +sentence-case@^2.1.0: + version "2.1.1" + resolved "http://registry.npm.taobao.org/sentence-case/download/sentence-case-2.1.1.tgz#1f6e2dda39c168bf92d13f86d4a918933f667ed4" + dependencies: + no-case "^2.2.0" + upper-case-first "^1.1.2" + +serialize-json@^1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/serialize-json/download/serialize-json-1.0.2.tgz#32638d9eabe8a2a00636186179ca8c622a22d043" + dependencies: + debug "^3.0.1" + is-type-of "^1.2.0" + utility "^1.12.0" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/set-blocking/download/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-value@^0.4.3: + version "0.4.3" + resolved "http://registry.npm.taobao.org/set-value/download/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/set-value/download/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/shebang-command/download/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/shebang-regex/download/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +shelljs@0.8.2: + version "0.8.2" + resolved "http://registry.npm.taobao.org/shelljs/download/shelljs-0.8.2.tgz#345b7df7763f4c2340d584abb532c5f752ca9e35" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "http://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +slash@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/slash/download/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +slice-ansi@1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/slice-ansi/download/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" + dependencies: + is-fullwidth-code-point "^2.0.0" + +sliced@1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/sliced/download/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + +smart-buffer@^1.0.13: + version "1.1.15" + resolved "http://registry.npm.taobao.org/smart-buffer/download/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16" + +snake-case@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/snake-case/download/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" + dependencies: + no-case "^2.2.0" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/snapdragon-node/download/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "http://registry.npm.taobao.org/snapdragon-util/download/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "http://registry.npm.taobao.org/snapdragon/download/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +socks-proxy-agent@^3.0.0: + version "3.0.1" + resolved "http://registry.npm.taobao.org/socks-proxy-agent/download/socks-proxy-agent-3.0.1.tgz#2eae7cf8e2a82d34565761539a7f9718c5617659" + dependencies: + agent-base "^4.1.0" + socks "^1.1.10" + +socks@^1.1.10: + version "1.1.10" + resolved "http://registry.npm.taobao.org/socks/download/socks-1.1.10.tgz#5b8b7fc7c8f341c53ed056e929b7bf4de8ba7b5a" + dependencies: + ip "^1.1.4" + smart-buffer "^1.0.13" + +sort-keys@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/sort-keys/download/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + dependencies: + is-plain-obj "^1.0.0" + +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "http://registry.npm.taobao.org/source-map-resolve/download/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.4.0, source-map-support@^0.4.15: + version "0.4.18" + resolved "http://registry.npm.taobao.org/source-map-support/download/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + dependencies: + source-map "^0.5.6" + +source-map-support@^0.5.4, source-map-support@^0.5.6, source-map-support@^0.5.9: + version "0.5.9" + resolved "http://registry.npm.taobao.org/source-map-support/download/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "http://registry.npm.taobao.org/source-map-url/download/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + +source-map@^0.1.34: + version "0.1.43" + resolved "http://registry.npm.taobao.org/source-map/download/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: + version "0.5.7" + resolved "http://registry.npm.taobao.org/source-map/download/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "http://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "http://registry.npm.taobao.org/sparse-bitfield/download/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + dependencies: + memory-pager "^1.0.2" + +spawn-wrap@^1.4.2: + version "1.4.2" + resolved "http://registry.npm.taobao.org/spawn-wrap/download/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c" + dependencies: + foreground-child "^1.5.6" + mkdirp "^0.5.0" + os-homedir "^1.0.1" + rimraf "^2.6.2" + signal-exit "^3.0.2" + which "^1.3.0" + +spdx-correct@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/spdx-correct/download/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/spdx-exceptions/download/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/spdx-expression-parse/download/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.1" + resolved "http://registry.npm.taobao.org/spdx-license-ids/download/spdx-license-ids-3.0.1.tgz#e2a303236cac54b04031fa7a5a79c7e701df852f" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "http://registry.npm.taobao.org/split-string/download/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + dependencies: + extend-shallow "^3.0.0" + +split2@^2.0.0, split2@^2.1.0, split2@^2.2.0: + version "2.2.0" + resolved "http://registry.npm.taobao.org/split2/download/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" + dependencies: + through2 "^2.0.2" + +split@^1.0.0, split@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/split/download/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + dependencies: + through "2" + +sprintf-js@1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/sprintf-js/download/sprintf-js-1.1.0.tgz#cffcaf702daf65ea39bb4e0fa2b299cec1a1be46" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "http://registry.npm.taobao.org/sprintf-js/download/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +stack-trace@0.0.10, stack-trace@^0.0.10: + version "0.0.10" + resolved "http://registry.npm.taobao.org/stack-trace/download/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + +static-extend@^0.1.1: + version "0.1.2" + resolved "http://registry.npm.taobao.org/static-extend/download/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.3.1, statuses@^1.5.0: + version "1.5.0" + resolved "http://registry.npm.taobao.org/statuses/download/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + +stream-combiner@^0.2.2: + version "0.2.2" + resolved "http://registry.npm.taobao.org/stream-combiner/download/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" + dependencies: + duplexer "~0.1.1" + through "~2.3.4" + +stream-http@^2.8.0: + version "2.8.3" + resolved "http://registry.npm.taobao.org/stream-http/download/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/stream-shift/download/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +stream-slice@^0.1.2: + version "0.1.2" + resolved "http://registry.npm.taobao.org/stream-slice/download/stream-slice-0.1.2.tgz#2dc4f4e1b936fb13f3eb39a2def1932798d07a4b" + +stream-wormhole@^1.0.4, stream-wormhole@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/stream-wormhole/download/stream-wormhole-1.1.0.tgz#300aff46ced553cfec642a05251885417693c33d" + +streamsearch@0.1.2: + version "0.1.2" + resolved "http://registry.npm.taobao.org/streamsearch/download/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/strict-uri-encode/download/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "http://registry.npm.taobao.org/string-width/download/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/string-width/download/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "http://registry.npm.taobao.org/string_decoder/download/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/string_decoder/download/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +stringifier@^1.3.0: + version "1.4.0" + resolved "http://registry.npm.taobao.org/stringifier/download/stringifier-1.4.0.tgz#d704581567f4526265d00ed8ecb354a02c3fec28" + dependencies: + core-js "^2.0.0" + traverse "^0.6.6" + type-name "^2.0.1" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/strip-bom/download/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/strip-bom/download/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/strip-eof/download/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/strip-indent/download/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-indent@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/strip-indent/download/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/strip-json-comments/download/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +superagent@^3.8.0, superagent@^3.8.3: + version "3.8.3" + resolved "http://registry.npm.taobao.org/superagent/download/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.2.0" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.3.5" + +supertest@^3.3.0: + version "3.3.0" + resolved "http://registry.npm.taobao.org/supertest/download/supertest-3.3.0.tgz#79b27bd7d34392974ab33a31fa51a3e23385987e" + dependencies: + methods "^1.1.2" + superagent "^3.8.3" + +supports-color@5.4.0: + version "5.4.0" + resolved "http://registry.npm.taobao.org/supports-color/download/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + dependencies: + has-flag "^3.0.0" + +supports-color@5.5.0, supports-color@^5.3.0, supports-color@^5.4.0: + version "5.5.0" + resolved "http://registry.npm.taobao.org/supports-color/download/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + dependencies: + has-flag "^3.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/supports-color/download/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +swap-case@^1.1.0: + version "1.1.2" + resolved "http://registry.npm.taobao.org/swap-case/download/swap-case-1.1.2.tgz#c39203a4587385fad3c850a0bd1bcafa081974e3" + dependencies: + lower-case "^1.1.1" + upper-case "^1.1.1" + +table@4.0.2: + version "4.0.2" + resolved "http://registry.npm.taobao.org/table/download/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" + dependencies: + ajv "^5.2.3" + ajv-keywords "^2.1.0" + chalk "^2.1.0" + lodash "^4.17.4" + slice-ansi "1.0.0" + string-width "^2.1.1" + +tar@^4: + version "4.4.6" + resolved "http://registry.npm.taobao.org/tar/download/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" + dependencies: + chownr "^1.0.1" + fs-minipass "^1.2.5" + minipass "^2.3.3" + minizlib "^1.1.0" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.2" + +tcp-base@^3.1.0: + version "3.1.0" + resolved "http://registry.npm.taobao.org/tcp-base/download/tcp-base-3.1.0.tgz#35458bf8c0a21a0cacf5092b4c758a53b39317c1" + dependencies: + is-type-of "^1.0.0" + sdk-base "^3.1.1" + +tcp-proxy.js@^1.0.5: + version "1.2.0" + resolved "http://registry.npm.taobao.org/tcp-proxy.js/download/tcp-proxy.js-1.2.0.tgz#1cabac4d83b518020457ef35151ba3e352daedec" + dependencies: + debug "^3.0.1" + through2 "^2.0.3" + +term-size@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/term-size/download/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" + dependencies: + execa "^0.7.0" + +test-exclude@^5.0.0: + version "5.0.0" + resolved "http://registry.npm.taobao.org/test-exclude/download/test-exclude-5.0.0.tgz#cdce7cece785e0e829cd5c2b27baf18bc583cfb7" + dependencies: + arrify "^1.0.1" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^1.0.1" + +text-extensions@^1.0.0: + version "1.8.0" + resolved "http://registry.npm.taobao.org/text-extensions/download/text-extensions-1.8.0.tgz#6f343c62268843019b21a616a003557bdb952d2b" + +text-table@~0.2.0: + version "0.2.0" + resolved "http://registry.npm.taobao.org/text-table/download/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +thenify-all@^1.0.0: + version "1.6.0" + resolved "http://registry.npm.taobao.org/thenify-all/download/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4", thenify@^3.2.1: + version "3.3.0" + resolved "http://registry.npm.taobao.org/thenify/download/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" + dependencies: + any-promise "^1.0.0" + +throat@^4.0.0: + version "4.1.0" + resolved "http://registry.npm.taobao.org/throat/download/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + +through2@^2.0.0, through2@^2.0.1, through2@^2.0.2, through2@^2.0.3: + version "2.0.3" + resolved "http://registry.npm.taobao.org/through2/download/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.4: + version "2.3.8" + resolved "http://registry.npm.taobao.org/through/download/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +thunkify@^2.1.2: + version "2.1.2" + resolved "http://registry.npm.taobao.org/thunkify/download/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d" + +timed-out@4.0.1, timed-out@^4.0.0, timed-out@^4.0.1: + version "4.0.1" + resolved "http://registry.npm.taobao.org/timed-out/download/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + +title-case@^2.1.0: + version "2.1.1" + resolved "http://registry.npm.taobao.org/title-case/download/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" + dependencies: + no-case "^2.2.0" + upper-case "^1.0.3" + +tmp-promise@1.0.5: + version "1.0.5" + resolved "http://registry.npm.taobao.org/tmp-promise/download/tmp-promise-1.0.5.tgz#3208d7fa44758f86a2a4c4060f3c33fea30e8038" + dependencies: + bluebird "^3.5.0" + tmp "0.0.33" + +tmp@0.0.33, tmp@^0.0.33: + version "0.0.33" + resolved "http://registry.npm.taobao.org/tmp/download/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/to-arraybuffer/download/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/to-fast-properties/download/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/to-fast-properties/download/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + +to-object-path@^0.3.0: + version "0.3.0" + resolved "http://registry.npm.taobao.org/to-object-path/download/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "http://registry.npm.taobao.org/to-regex-range/download/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "http://registry.npm.taobao.org/to-regex/download/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/toidentifier/download/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + +traverse@^0.6.6: + version "0.6.6" + resolved "http://registry.npm.taobao.org/traverse/download/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/trim-newlines/download/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim-newlines@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/trim-newlines/download/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" + +trim-off-newlines@^1.0.0: + version "1.0.1" + resolved "http://registry.npm.taobao.org/trim-off-newlines/download/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" + +trim-right@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/trim-right/download/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +ts-node@^7.0.0, ts-node@^7.0.1: + version "7.0.1" + resolved "http://registry.npm.taobao.org/ts-node/download/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" + dependencies: + arrify "^1.0.0" + buffer-from "^1.1.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.6" + yn "^2.0.0" + +tslib@^1.9.0: + version "1.9.3" + resolved "http://registry.npm.taobao.org/tslib/download/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + +tsscmp@1.0.5: + version "1.0.5" + resolved "http://registry.npm.taobao.org/tsscmp/download/tsscmp-1.0.5.tgz#7dc4a33af71581ab4337da91d85ca5427ebd9a97" + +type-check@~0.3.2: + version "0.3.2" + resolved "http://registry.npm.taobao.org/type-check/download/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-is@^1.6.15, type-is@^1.6.16: + version "1.6.16" + resolved "http://registry.npm.taobao.org/type-is/download/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.18" + +type-name@^2.0.0, type-name@^2.0.1: + version "2.0.2" + resolved "http://registry.npm.taobao.org/type-name/download/type-name-2.0.2.tgz#efe7d4123d8ac52afff7f40c7e4dec5266008fb4" + +typedarray@^0.0.6: + version "0.0.6" + resolved "http://registry.npm.taobao.org/typedarray/download/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uglify-js@^3.1.4: + version "3.4.9" + resolved "http://registry.npm.taobao.org/uglify-js/download/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + +uid-safe@2.1.4: + version "2.1.4" + resolved "http://registry.npm.taobao.org/uid-safe/download/uid-safe-2.1.4.tgz#3ad6f38368c6d4c8c75ec17623fb79aa1d071d81" + dependencies: + random-bytes "~1.0.0" + +uid-safe@^2.1.3: + version "2.1.5" + resolved "http://registry.npm.taobao.org/uid-safe/download/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + dependencies: + random-bytes "~1.0.0" + +ultron@1.0.x: + version "1.0.2" + resolved "http://registry.npm.taobao.org/ultron/download/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" + +unescape@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/unescape/download/unescape-1.0.1.tgz#956e430f61cad8a4d57d82c518f5e6cc5d0dda96" + dependencies: + extend-shallow "^2.0.1" + +union-value@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/union-value/download/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" + +unique-string@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/unique-string/download/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + dependencies: + crypto-random-string "^1.0.0" + +universal-deep-strict-equal@^1.2.1: + version "1.2.2" + resolved "http://registry.npm.taobao.org/universal-deep-strict-equal/download/universal-deep-strict-equal-1.2.2.tgz#0da4ac2f73cff7924c81fa4de018ca562ca2b0a7" + dependencies: + array-filter "^1.0.0" + indexof "0.0.1" + object-keys "^1.0.0" + +unpipe@1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/unpipe/download/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +unset-value@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/unset-value/download/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +unzip-response@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/unzip-response/download/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" + +upath@^1.0.5: + version "1.1.0" + resolved "http://registry.npm.taobao.org/upath/download/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" + +update-notifier@2.5.0: + version "2.5.0" + resolved "http://registry.npm.taobao.org/update-notifier/download/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" + dependencies: + boxen "^1.2.1" + chalk "^2.0.1" + configstore "^3.0.0" + import-lazy "^2.1.0" + is-ci "^1.0.10" + is-installed-globally "^0.1.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + +upper-case-first@^1.1.0, upper-case-first@^1.1.2: + version "1.1.2" + resolved "http://registry.npm.taobao.org/upper-case-first/download/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" + dependencies: + upper-case "^1.1.1" + +upper-case@^1.0.3, upper-case@^1.1.0, upper-case@^1.1.1, upper-case@^1.1.3: + version "1.1.3" + resolved "http://registry.npm.taobao.org/upper-case/download/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + +urijs@^1.19.0: + version "1.19.1" + resolved "http://registry.npm.taobao.org/urijs/download/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" + +urix@^0.1.0: + version "0.1.0" + resolved "http://registry.npm.taobao.org/urix/download/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/url-parse-lax/download/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + dependencies: + prepend-http "^1.0.1" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/url-parse-lax/download/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + dependencies: + prepend-http "^2.0.0" + +url-template@^2.0.8: + version "2.0.8" + resolved "http://registry.npm.taobao.org/url-template/download/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" + +url-to-options@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/url-to-options/download/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" + +urllib@^2.17.1, urllib@^2.24.0, urllib@^2.25.1, urllib@^2.29.1: + version "2.29.1" + resolved "http://registry.npm.taobao.org/urllib/download/urllib-2.29.1.tgz#2164cc1f47cb8351fc77954a5017b1b3a2a046ed" + dependencies: + any-promise "^1.3.0" + content-type "^1.0.2" + debug "^2.6.0" + default-user-agent "^1.0.0" + digest-header "^0.0.1" + ee-first "~1.1.1" + humanize-ms "^1.2.0" + iconv-lite "^0.4.15" + ip "^1.1.5" + proxy-agent "^2.1.0" + pump "^3.0.0" + qs "^6.4.0" + statuses "^1.3.1" + utility "^1.12.0" + +use@^3.1.0: + version "3.1.1" + resolved "http://registry.npm.taobao.org/use/download/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "http://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +utility@0.1.11: + version "0.1.11" + resolved "http://registry.npm.taobao.org/utility/download/utility-0.1.11.tgz#fde60cf9b4e4751947a0cf5d104ce29367226715" + dependencies: + address ">=0.0.1" + +utility@^1.11.0, utility@^1.12.0, utility@^1.13.1, utility@^1.14.0, utility@^1.8.0: + version "1.15.0" + resolved "http://registry.npm.taobao.org/utility/download/utility-1.15.0.tgz#660d81c656a3c50e3c3b75d5fc440d74fa876dfa" + dependencies: + copy-to "^2.0.1" + escape-html "^1.0.3" + mkdirp "^0.5.1" + mz "^2.7.0" + unescape "^1.0.1" + +uuid@3.3.2, uuid@^3.0.1, uuid@^3.3.2: + version "3.3.2" + resolved "http://registry.npm.taobao.org/uuid/download/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "http://registry.npm.taobao.org/validate-npm-package-license/download/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +validator@^10.6.0: + version "10.7.1" + resolved "http://registry.npm.taobao.org/validator/download/validator-10.7.1.tgz#dd4cc750c2134ce4a15a2acfc7b233669d659c5b" + +vary@^1.1.2: + version "1.1.2" + resolved "http://registry.npm.taobao.org/vary/download/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/wcwidth/download/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + dependencies: + defaults "^1.0.3" + +webstorm-disable-index@^1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/webstorm-disable-index/download/webstorm-disable-index-1.2.0.tgz#03d55abe05aff34a6af12b7bfddcaee145c64bd0" + dependencies: + co "^4.6.0" + lodash "^4.17.2" + mkdirp "^0.5.1" + mz "^2.6.0" + xml-mapping "^1.7.1" + +which-module@^1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/which-module/download/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which-module@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/which-module/download/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.2.9, which@^1.3.0: + version "1.3.1" + resolved "http://registry.npm.taobao.org/which/download/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "http://registry.npm.taobao.org/wide-align/download/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + dependencies: + string-width "^1.0.2 || 2" + +widest-line@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/widest-line/download/widest-line-2.0.0.tgz#0142a4e8a243f8882c0233aa0e0281aa76152273" + dependencies: + string-width "^2.1.1" + +win-release@^1.0.0: + version "1.1.1" + resolved "http://registry.npm.taobao.org/win-release/download/win-release-1.1.1.tgz#5fa55e02be7ca934edfc12665632e849b72e5209" + dependencies: + semver "^5.0.1" + +window-size@1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/window-size/download/window-size-1.1.1.tgz#9858586580ada78ab26ecd6978a6e03115c1af20" + dependencies: + define-property "^1.0.0" + is-number "^3.0.0" + +window-size@^0.1.4: + version "0.1.4" + resolved "http://registry.npm.taobao.org/window-size/download/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "http://registry.npm.taobao.org/wordwrap/download/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/wordwrap/download/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "http://registry.npm.taobao.org/wrappy/download/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write-file-atomic@^2.0.0: + version "2.3.0" + resolved "http://registry.npm.taobao.org/write-file-atomic/download/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write@^0.2.1: + version "0.2.1" + resolved "http://registry.npm.taobao.org/write/download/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +ws@^1.1.5: + version "1.1.5" + resolved "http://registry.npm.taobao.org/ws/download/ws-1.1.5.tgz#cbd9e6e75e09fc5d2c90015f21f0c40875e0dd51" + dependencies: + options ">=0.0.5" + ultron "1.0.x" + +wt@^1.1.1: + version "1.2.0" + resolved "http://registry.npm.taobao.org/wt/download/wt-1.2.0.tgz#b4cbe34c1f50a56a5433a9dda8cbdb7c62e3bcdf" + dependencies: + debug "^2.2.0" + ndir "^0.1.5" + sdk-base "^2.0.1" + +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "http://registry.npm.taobao.org/xdg-basedir/download/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + +xml-mapping@^1.7.1: + version "1.7.1" + resolved "http://registry.npm.taobao.org/xml-mapping/download/xml-mapping-1.7.1.tgz#65689659e5085833c7d2bec57dac2842ccbbc286" + dependencies: + sax "=0.4.2" + xml-writer ">=1.0.4" + +xml-writer@>=1.0.4: + version "1.7.0" + resolved "http://registry.npm.taobao.org/xml-writer/download/xml-writer-1.7.0.tgz#b76f1d591c16a2634ebdb703c7bdbd0fd6819065" + +xml2js@^0.4.16: + version "0.4.19" + resolved "http://registry.npm.taobao.org/xml2js/download/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "http://registry.npm.taobao.org/xmlbuilder/download/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + +xregexp@2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/xregexp/download/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" + +xss@^0.3.4: + version "0.3.8" + resolved "http://registry.npm.taobao.org/xss/download/xss-0.3.8.tgz#d0cbe23bde490bc98c139f08de3899165a68af0e" + dependencies: + commander "^2.9.0" + cssfilter "0.0.10" + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.1" + resolved "http://registry.npm.taobao.org/xtend/download/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.0, y18n@^3.2.1: + version "3.2.1" + resolved "http://registry.npm.taobao.org/y18n/download/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "http://registry.npm.taobao.org/yallist/download/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yallist@^3.0.0, yallist@^3.0.2: + version "3.0.2" + resolved "http://registry.npm.taobao.org/yallist/download/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" + +yargs-parser@10.1.0: + version "10.1.0" + resolved "http://registry.npm.taobao.org/yargs-parser/download/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + dependencies: + camelcase "^4.1.0" + +yargs-parser@^4.2.0: + version "4.2.1" + resolved "http://registry.npm.taobao.org/yargs-parser/download/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^5.0.0: + version "5.0.0" + resolved "http://registry.npm.taobao.org/yargs-parser/download/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "http://registry.npm.taobao.org/yargs-parser/download/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs-parser@^9.0.2: + version "9.0.2" + resolved "http://registry.npm.taobao.org/yargs-parser/download/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" + dependencies: + camelcase "^4.1.0" + +yargs@11.1.0: + version "11.1.0" + resolved "http://registry.npm.taobao.org/yargs/download/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77" + dependencies: + cliui "^4.0.0" + decamelize "^1.1.1" + find-up "^2.1.0" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^9.0.2" + +yargs@^3.32.0: + version "3.32.0" + resolved "http://registry.npm.taobao.org/yargs/download/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" + dependencies: + camelcase "^2.0.1" + cliui "^3.0.3" + decamelize "^1.1.1" + os-locale "^1.4.0" + string-width "^1.0.1" + window-size "^0.1.4" + y18n "^3.2.0" + +yargs@^6.0.0: + version "6.6.0" + resolved "http://registry.npm.taobao.org/yargs/download/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^4.2.0" + +yargs@^7.0.1: + version "7.1.0" + resolved "http://registry.npm.taobao.org/yargs/download/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.0" + +yargs@^8.0.2: + version "8.0.2" + resolved "http://registry.npm.taobao.org/yargs/download/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yauzl@^2.9.2: + version "2.10.0" + resolved "http://registry.npm.taobao.org/yauzl/download/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +ylru@^1.2.0, ylru@^1.2.1: + version "1.2.1" + resolved "http://registry.npm.taobao.org/ylru/download/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f" + +yn@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/yn/download/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" + +ypkgfiles@^1.6.0: + version "1.6.0" + resolved "http://registry.npm.taobao.org/ypkgfiles/download/ypkgfiles-1.6.0.tgz#6bae5566160a6c934f573501987f691624506351" + dependencies: + debug "^2.6.1" + glob "^7.1.1" + is-type-of "^1.0.0" + resolve-files "^1.0.0" + yargs "^7.0.1" + +zlib@^1.0.5: + version "1.0.5" + resolved "http://registry.npm.taobao.org/zlib/download/zlib-1.0.5.tgz#6e7c972fc371c645a6afb03ab14769def114fcc0" + +zlogger@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/zlogger/download/zlogger-1.1.0.tgz#bc0d4a1a50d88b4e027636060c7a23682754744e" + dependencies: + pumpify "^1.3.5" + split2 "^2.1.0" + through2 "^2.0.1" From 19cd1456621464f6b726e9f085ce54cbe4a81412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 30 Sep 2018 23:49:52 +0800 Subject: [PATCH 180/208] fix: fix github setting --- app/service/setting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/service/setting.js b/app/service/setting.js index 8d669c8..0850866 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -86,7 +86,7 @@ module.exports = class SettingService extends ProxyService { let setting = await this.getItem() if (!setting) return null const github = setting.personal.github - if (!github.login) return + if (!github || !github.login) return const user = await this.service.github.getUserInfo(github.login) if (!user) return setting = await this.updateItemById(setting._id, { From 416a4e788926bca4a10f1141d3cb6120fc171e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 3 Oct 2018 23:34:12 +0800 Subject: [PATCH 181/208] fix: fix bug --- app/controller/comment.js | 33 ++++++++++++++++++++----------- app/controller/notification.js | 7 +++++++ app/extend/context.js | 3 +-- app/service/article.js | 36 ++++++++++++---------------------- app/service/comment.js | 4 ++-- app/service/user.js | 18 +++++------------ app/utils/validate.js | 11 +++++++---- 7 files changed, 57 insertions(+), 55 deletions(-) diff --git a/app/controller/comment.js b/app/controller/comment.js index 0bc3a57..6c2cbbc 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -42,7 +42,7 @@ module.exports = class CommentController extends Controller { async list () { const { ctx } = this ctx.query.page = Number(ctx.query.page) - const tranArray = ['limit', 'state', 'type', 'order', 'sortBy'] + const tranArray = ['limit', 'state', 'type', 'order'] tranArray.forEach(key => { if (ctx.query[key]) { ctx.query[key] = Number(ctx.query[key]) @@ -103,6 +103,12 @@ module.exports = class CommentController extends Controller { query.parent = { $exists: false } } + // 排序 + if (sortBy && order) { + options.sort = {} + options.sort[sortBy] = order + } + // 未通过权限校验(前台获取文章列表) if (!ctx.session._isAuthed) { // 将评论状态重置为1 @@ -111,12 +117,6 @@ module.exports = class CommentController extends Controller { // 评论列表不需要content和state options.select = '-content -state -updatedAt -spam -type -meta.ip' } else { - // 排序 - if (sortBy && order) { - options.sort = {} - options.sort[sortBy] = order - } - // 起始日期 if (startDate) { const $gte = new Date(startDate) @@ -134,9 +134,20 @@ module.exports = class CommentController extends Controller { } } const data = await this.service.comment.getLimitListByQuery(ctx.processPayload(query), options) - data - ? ctx.success(data, '评论列表获取成功') - : ctx.fail('评论列表获取失败') + if (!data) { + return ctx.fail('评论列表获取失败') + } + + let { list, pageInfo } = data + list = await Promise.all( + list.map(async doc => { + doc.subCount = 0 + const count = await this.service.comment.count({ parent: doc._id }) + doc.subCount = count + return doc + }) + ) + ctx.success({ list, pageInfo }, '评论列表获取成功') } async item () { @@ -171,7 +182,7 @@ module.exports = class CommentController extends Controller { return ctx.fail(error) } else if (user.mute) { // 被禁言 - return ctx.fail('该用户已被禁言') + return ctx.fail('你已被禁言,请联系管理员解禁') } body.author = user._id const spamValid = await this.service.user.checkUserSpam(user) diff --git a/app/controller/notification.js b/app/controller/notification.js index 401c876..5328858 100644 --- a/app/controller/notification.js +++ b/app/controller/notification.js @@ -54,6 +54,13 @@ module.exports = class NotificationController extends Controller { select: '-password' }, { path: 'target.comment', + populate: [ + { + path: 'article' + }, { + path: 'author' + } + ] }, { path: 'actors.from', select: '-password' diff --git a/app/extend/context.js b/app/extend/context.js index e578c6c..6a41380 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -47,8 +47,7 @@ module.exports = { if (isObject(author)) { this.validate({ name: 'string', - email: 'string', - site: { type: 'string', required: false } + email: 'string' }, author) } else if (!isObjectId(author)) { this.throw(422, '发布人不存在') diff --git a/app/service/article.js b/app/service/article.js index 4832617..b936386 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -139,31 +139,21 @@ module.exports = class ArticleService extends ProxyService { if (!this.ctx.session._isAuthed) { query.state = this.config.modelEnum.article.state.optional.PUBLISH } - const prev = await this.getItem( - query, - 'title createdAt publishedAt thumb category', - { - sort: 'createdAt' - }, - { - path: 'category', - select: 'name description' + const nextQuery = Object.assign({}, query, { + createdAt: { + $gt: data.createdAt } - ) - query.createdAt = { - $gt: data.createdAt + }) + const select = '-renderedContent' + const opt = { sort: 'createdAt' } + const populate = { + path: 'category', + select: 'name description' } - const next = await this.getItem( - query, - 'title createdAt publishedAt thumb category', - { - sort: 'createdAt' - }, - { - path: 'category', - select: 'name description' - } - ) + const [prev, next] = await Promise.all([ + this.getItem(query, select, opt, populate), + this.getItem(nextQuery, select, opt, populate) + ]) return { prev: prev || null, next: next || null diff --git a/app/service/comment.js b/app/service/comment.js index 1dc8d1a..90b82fa 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -14,7 +14,7 @@ module.exports = class CommentService extends ProxyService { const populate = [ { path: 'author', - select: 'github name avatar' + select: 'github name avatar email site' }, { path: 'parent', select: 'author meta sticky ups' @@ -29,7 +29,7 @@ module.exports = class CommentService extends ProxyService { if (!this.ctx.session._isAuthed) { data = await this.getItem( { _id: id, state: 1, spam: false }, - '-content -state -updatedAt -type -spam', + '-content -state -updatedAt -spam', null, populate ) diff --git a/app/service/user.js b/app/service/user.js index 989187f..91e6abf 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -78,20 +78,12 @@ module.exports = class UserService extends ProxyService { } } } else { - user = await this.getItem({ name: author.name }) + user = await this.create(Object.assign(update, { + role: this.config.modelEnum.user.role.optional.NORMAL + })) if (user) { - // 名称重复,不能创建评论 - user = null - error = '用户名重复,请修改后再提交' - } else { - // 可以创建 - user = await this.create(Object.assign(update, { - role: this.config.modelEnum.user.role.optional.NORMAL - })) - if (user) { - this.service.notification.recordUser(user, 'create') - this.service.stat.record('USER_CREATE', { user: user._id }, 'count') - } + this.service.notification.recordUser(user, 'create') + this.service.stat.record('USER_CREATE', { user: user._id }, 'count') } } } diff --git a/app/utils/validate.js b/app/utils/validate.js index e1162e3..c9a1675 100644 --- a/app/utils/validate.js +++ b/app/utils/validate.js @@ -27,7 +27,10 @@ Object.keys(validator).forEach(key => { } }) -exports.isUrl = (site = '') => validator.isURL(site, { - protocols: ['http', 'https'], - require_protocol: true -}) +exports.isUrl = (site = '') => { + if (!site) return true + return validator.isURL(site, { + protocols: ['http', 'https'], + require_protocol: true + }) +} From 34770dcbdd43854f82c77419d3eefe207489c21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 3 Oct 2018 23:34:27 +0800 Subject: [PATCH 182/208] [chore] release 2.0.0-alpha.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 721213e..ffaa070 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "2.0.0-alpha.3", + "version": "2.0.0-alpha.4", "description": "", "private": true, "dependencies": { From 2f9982be7d1f76aa2d4f9cb87518967741dbc50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 5 Oct 2018 22:03:30 +0800 Subject: [PATCH 183/208] fix: fix text --- app/controller/comment.js | 12 ++++++++---- app/service/article.js | 8 +++++--- app/service/comment.js | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/controller/comment.js b/app/controller/comment.js index 6c2cbbc..322480c 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -134,8 +134,10 @@ module.exports = class CommentController extends Controller { } } const data = await this.service.comment.getLimitListByQuery(ctx.processPayload(query), options) + const commentType = this.config.modelEnum.comment.type.optional.COMMENT + const typeText = type === commentType ? '评论' : '留言' if (!data) { - return ctx.fail('评论列表获取失败') + return ctx.fail(typeText + '列表获取失败') } let { list, pageInfo } = data @@ -147,7 +149,7 @@ module.exports = class CommentController extends Controller { return doc }) ) - ctx.success({ list, pageInfo }, '评论列表获取成功') + ctx.success({ list, pageInfo }, typeText + '列表获取成功') } async item () { @@ -224,8 +226,10 @@ module.exports = class CommentController extends Controller { // 如果是文章评论,则更新文章评论数量 this.service.article.updateCommentCount(data.article._id) } - // 发送邮件通知站主和被评论者 - this.service.comment.sendCommentEmailToAdminAndUser(data) + if (this.config.isProd) { + // 发送邮件通知站主和被评论者 + this.service.comment.sendCommentEmailToAdminAndUser(data) + } // 生成通告 this.service.notification.recordComment(comment, 'create') ctx.success(data, data.type === COMMENT ? '评论发布成功' : '留言发布成功') diff --git a/app/service/article.js b/app/service/article.js index b936386..20f6224 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -52,7 +52,8 @@ module.exports = class ArticleService extends ProxyService { year: { $year: '$createdAt' }, month: { $month: '$createdAt' }, title: 1, - createdAt: 1 + createdAt: 1, + source: 1 } if (!this.ctx.session._isAuthed) { $match.state = 1 @@ -74,7 +75,8 @@ module.exports = class ArticleService extends ProxyService { title: '$title', _id: '$_id', createdAt: '$createdAt', - state: '$state' + state: '$state', + source: '$source' } } } @@ -97,7 +99,7 @@ module.exports = class ArticleService extends ProxyService { }) return { year, - months + months: months.sort((a, b) => b.month - a.month) } }) } diff --git a/app/service/comment.js b/app/service/comment.js index 90b82fa..0efb542 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -80,7 +80,7 @@ module.exports = class CommentService extends ProxyService { if (forwardAuthor && forwardAuthor.email) { this.service.mail.send(typeTitle, { to: forwardAuthor.email, - subject: `你在 ${this.config.author.name} 的博客的评论有了新的回复`, + subject: '你在 Jooger.me 的博客的评论有了新的回复', text: `来自 ${comment.author.name} 的回复:${comment.content}`, html: `

来自 ${comment.author.name} 的回复 => 点击查看:${comment.renderedContent}

` }) From 071f0115e1d83b3ddb66f27866335a485f8a7f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sat, 6 Oct 2018 16:28:11 +0800 Subject: [PATCH 184/208] fix: setting data add github cover --- app/controller/setting.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/controller/setting.js b/app/controller/setting.js index ede8671..7064bfd 100644 --- a/app/controller/setting.js +++ b/app/controller/setting.js @@ -49,9 +49,14 @@ module.exports = class SettingController extends Controller { null, populate ) - data - ? ctx.success(data, '配置获取成功') - : ctx.fail('配置获取失败') + if (data) { + if (!data.personal.github) { + data.personal.github = {} + } + ctx.success(data, '配置获取成功') + } else { + ctx.fail('配置获取失败') + } } async update () { From afbf5c7e87eee836787926c362bfefd372e46876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sat, 6 Oct 2018 21:07:59 +0800 Subject: [PATCH 185/208] fix: fix akismet error and fix user create service --- app/service/akismet.js | 2 +- app/service/user.js | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/service/akismet.js b/app/service/akismet.js index 093f4e6..416d703 100644 --- a/app/service/akismet.js +++ b/app/service/akismet.js @@ -13,7 +13,7 @@ module.exports = class AkismetService extends Service { if (err) { this.app.coreLogger.error('评论验证失败,将跳过Spam验证,错误:', err.message) this.service.notification.recordGeneral('AKISMET', 'CHECK_FAIL', err) - return resolve(true) + return resolve(false) } if (spam) { this.app.coreLogger.warn('评论验证不通过,疑似垃圾评论') diff --git a/app/service/user.js b/app/service/user.js index 91e6abf..babf07d 100644 --- a/app/service/user.js +++ b/app/service/user.js @@ -28,12 +28,14 @@ module.exports = class UserService extends ProxyService { } // 创建用户 - async create (user) { + async create (user, checkExist = true) { const { name } = user - const exist = await this.getItem({ name }) - if (exist) { - this.logger.info('用户已存在,无需创建:' + name) - return exist + if (checkExist) { + const exist = await this.getItem({ name }) + if (exist) { + this.logger.info('用户已存在,无需创建:' + name) + return exist + } } const data = await new this.model(user).save() const type = ['管理员', '用户'][data.role] @@ -80,7 +82,7 @@ module.exports = class UserService extends ProxyService { } else { user = await this.create(Object.assign(update, { role: this.config.modelEnum.user.role.optional.NORMAL - })) + }), false) if (user) { this.service.notification.recordUser(user, 'create') this.service.stat.record('USER_CREATE', { user: user._id }, 'count') From 715088241e594c6f6527d3ee20f766c60d2e8bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 7 Oct 2018 00:43:20 +0800 Subject: [PATCH 186/208] update: support async alinode see egg-alinode-async --- app.js | 6 +- app/lib/plugin/egg-alinode/agent.js | 5 -- .../egg-alinode/config/config.default.js | 39 ----------- app/lib/plugin/egg-alinode/lib/alinode.js | 46 ------------- app/lib/plugin/egg-alinode/package.json | 5 -- .../plugin/egg-alinode/schedule/removeLogs.js | 58 ----------------- app/service/aliApi.js | 2 +- config/plugin.prod.js | 10 ++- package.json | 3 +- yarn.lock | 64 +++++++++++++++++-- 10 files changed, 69 insertions(+), 169 deletions(-) delete mode 100644 app/lib/plugin/egg-alinode/agent.js delete mode 100644 app/lib/plugin/egg-alinode/config/config.default.js delete mode 100644 app/lib/plugin/egg-alinode/lib/alinode.js delete mode 100644 app/lib/plugin/egg-alinode/package.json delete mode 100644 app/lib/plugin/egg-alinode/schedule/removeLogs.js diff --git a/app.js b/app.js index b813f2f..cdf0be0 100644 --- a/app.js +++ b/app.js @@ -11,7 +11,11 @@ module.exports = app => { // 初始化管理员(如果有必要) await ctx.service.auth.seed() // 初始化配置(如果有必要) - await ctx.service.setting.seed() + const setting = await ctx.service.setting.seed() + // prod异步启动alinode + if (app.config.isProd) { + app.messenger.sendToAgent('alinode-run', setting.keys.alinode) + } }) } diff --git a/app/lib/plugin/egg-alinode/agent.js b/app/lib/plugin/egg-alinode/agent.js deleted file mode 100644 index 3957d6f..0000000 --- a/app/lib/plugin/egg-alinode/agent.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = agent => { - agent.logger.info(333) - agent.messenger.on('egg-ready', () => { - }) -} diff --git a/app/lib/plugin/egg-alinode/config/config.default.js b/app/lib/plugin/egg-alinode/config/config.default.js deleted file mode 100644 index cb0cd7d..0000000 --- a/app/lib/plugin/egg-alinode/config/config.default.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict' - -const path = require('path') -const mkdirp = require('mkdirp') - -module.exports = appInfo => { - const exports = {} - - const appRoot = appInfo.env === 'local' || appInfo.env === 'unittest' ? appInfo.baseDir : appInfo.HOME - let alinodeLogdir = path.join(appRoot, 'logs/alinode') - // try to use NODE_LOG_DIR first - if (process.env.NODE_LOG_DIR) { - alinodeLogdir = process.env.NODE_LOG_DIR - } - mkdirp.sync(alinodeLogdir) - - exports.alinode = { - enable: true, - // default is wss://agentserver.node.aliyun.com:8080 - server: 'wss://agentserver.node.aliyun.com:8080', - appid: '', - secret: '', - cmddir: path.dirname(require.resolve('commandx/package.json')), - logdir: alinodeLogdir, - error_log: [ - path.join(appRoot, `logs/${appInfo.pkg.name}/common-error.log`), - path.join(appRoot, 'logs/stderr.log'), - ], - packages: [ - path.join(appInfo.baseDir, 'package.json'), - ], - // seconds - reconnectDelay: 10, - heartbeatInterval: 60, - reportInterval: 60, - } - - return exports -} diff --git a/app/lib/plugin/egg-alinode/lib/alinode.js b/app/lib/plugin/egg-alinode/lib/alinode.js deleted file mode 100644 index 847c84c..0000000 --- a/app/lib/plugin/egg-alinode/lib/alinode.js +++ /dev/null @@ -1,46 +0,0 @@ -const assert = require('assert') -const AlinodeAgent = require('agentx') -const homedir = require('node-homedir') -const fs = require('fs') -const path = require('path') - -module.exports = agent => { - agent.addSingleton('alinode', createClient) -} - -function createClient (config, agent) { - if (!config.enable) { - agent.coreLogger.info('[egg-alinode] disable') - return - } - assert(config.appid, 'config.alinode.appid required') - assert(config.secret, 'config.alinode.secret required') - - const nodepathFile = path.join(homedir(), '.nodepath') - const nodeBin = path.dirname(process.execPath) - fs.writeFileSync(nodepathFile, nodeBin) - config.logger = agent.coreLogger - config.libMode = true - const client = new AlinodeAgent(config) - agent.beforeStart(async () => { - agent.coreLogger.info('[egg-alinode] alinode agentx started, node versions: %j, update %s with %j, config: %j', - process.versions, - nodepathFile, - nodeBin, { - server: config.server, - appid: config.appid, - } - ) - }) - return { - client, - config, - run () { - return this.client.run() - }, - restart (config) { - this.client = new AlinodeAgent(config || this.config) - this.config = config - } - } -} diff --git a/app/lib/plugin/egg-alinode/package.json b/app/lib/plugin/egg-alinode/package.json deleted file mode 100644 index d3e3553..0000000 --- a/app/lib/plugin/egg-alinode/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "eggPlugin": { - "name": "alinode" - } -} diff --git a/app/lib/plugin/egg-alinode/schedule/removeLogs.js b/app/lib/plugin/egg-alinode/schedule/removeLogs.js deleted file mode 100644 index 233815a..0000000 --- a/app/lib/plugin/egg-alinode/schedule/removeLogs.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict' - -const path = require('path') -const fs = require('mz/fs') -const moment = require('moment') - -module.exports = app => { - const exports = {} - - const logger = app.coreLogger - - exports.schedule = { - type: 'worker', // only one worker run this task - cron: '0 0 * * *', // run every day at 00:00 - } - exports.task = function* () { - const logdir = app.config.alinode.logdir - const maxDays = 7 - try { - yield removeExpiredLogFiles(logdir, maxDays) - } catch (err) { - logger.error(err) - } - } - - // remove expired log files: [access|node]-YYYYMMDD.log - function* removeExpiredLogFiles (logdir, maxDays) { - const files = yield fs.readdir(logdir) - const expriedDate = moment().subtract(maxDays, 'days').startOf('date') - const names = files.filter(name => { - const m = /^(?:access|node)\-(\d{8})\.log$/.exec(name) - if (!m) { - return false - } - const date = moment(m[1], 'YYYYMMDD').startOf('date') - if (!date.isValid()) { - return false - } - return date.isBefore(expriedDate) - }) - if (names.length === 0) { - return - } - - logger.info(`[egg-alinode] start remove ${logdir} files: ${names.join(', ')}`) - yield names.map(name => function* () { - const logfile = path.join(logdir, name) - try { - yield fs.unlink(logfile) - } catch (err) { - err.message = `[egg-alinode] remove logfile ${logfile} error, ${err.message}` - logger.error(err) - } - }) - } - - return exports -} diff --git a/app/service/aliApi.js b/app/service/aliApi.js index 50bd939..5a84c54 100644 --- a/app/service/aliApi.js +++ b/app/service/aliApi.js @@ -5,7 +5,7 @@ const https = require('https') const { Service } = require('egg') -module.exports = class MailService extends Service { +module.exports = class AliApiService extends Service { lookupIp (ip) { return new Promise(resolve => { const req = https.request({ diff --git a/config/plugin.prod.js b/config/plugin.prod.js index ff1fa86..170940b 100644 --- a/config/plugin.prod.js +++ b/config/plugin.prod.js @@ -1,13 +1,11 @@ 'use strict' -// const path = require('path') - exports.sentry = { enable: true, package: 'egg-sentry', } -// exports.alinode = { -// enable: true, -// path: path.join(__dirname, '../app/lib/plugin/egg-alinode') -// } +exports['alinode-async'] = { + enable: true, + package: 'egg-alinode-async' +} diff --git a/package.json b/package.json index ffaa070..178675f 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "description": "", "private": true, "dependencies": { - "agentx": "^1.9.11", "akismet-api": "^4.2.0", "bcryptjs": "^2.4.3", "egg": "^2.2.1", + "egg-alinode-async": "^2.1.2", "egg-console": "^2.0.1", "egg-cors": "^2.1.0", "egg-mongoose": "^3.1.0", @@ -29,7 +29,6 @@ "moment": "^2.22.2", "mongoose": "5.2.8", "mongoose-paginate-v2": "^1.0.12", - "node-homedir": "^1.1.1", "nodemailer": "^4.6.8", "validator": "^10.6.0", "zlib": "^1.0.5" diff --git a/yarn.lock b/yarn.lock index 09475a9..a6365f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -373,9 +373,9 @@ agentkeepalive@^3.4.1, agentkeepalive@^3.5.1: dependencies: humanize-ms "^1.2.1" -agentx@^1.9.11: - version "1.9.11" - resolved "http://registry.npm.taobao.org/agentx/download/agentx-1.9.11.tgz#1058138977a53d814d975791a2f5b5afc3c3f44b" +agentx@^1.9.3: + version "1.9.12" + resolved "http://registry.npm.taobao.org/agentx/download/agentx-1.9.12.tgz#e43565cc33b5f38402731ce5099b4d2f36cbbf88" dependencies: debug "^3.1.0" nounou "^1.2.1" @@ -1893,6 +1893,14 @@ commander@~2.17.1: version "2.17.1" resolved "http://registry.npm.taobao.org/commander/download/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" +commandx@^1.5.0: + version "1.5.4" + resolved "http://registry.npm.taobao.org/commandx/download/commandx-1.5.4.tgz#2e09187a93df845433e9ae791e9922a7b16d8f10" + dependencies: + formstream "^1.1.0" + tunnel-agent "^0.6.0" + urllib "^2.22.0" + common-bin@^2.7.1, common-bin@^2.7.3: version "2.7.3" resolved "http://registry.npm.taobao.org/common-bin/download/common-bin-2.7.3.tgz#9841b72d954ad0d62f08b0f530fde56aff0154e2" @@ -2537,6 +2545,17 @@ ee-first@1.1.1, ee-first@^1.1.1, ee-first@~1.1.1: version "1.1.1" resolved "http://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" +egg-alinode-async@^2.1.2: + version "2.1.2" + resolved "http://registry.npm.taobao.org/egg-alinode-async/download/egg-alinode-async-2.1.2.tgz#14387d9279d449987cc19ed75a34c6a1da5704b5" + dependencies: + agentx "^1.9.3" + commandx "^1.5.0" + mkdirp "^0.5.1" + moment "^2.22.1" + mz "^2.7.0" + node-homedir "^1.1.0" + egg-bin@^4.3.5: version "4.9.0" resolved "http://registry.npm.taobao.org/egg-bin/download/egg-bin-4.9.0.tgz#e6bc89c29bfda6ad7f80681ef92d9653c410692f" @@ -3502,6 +3521,14 @@ formidable@^1.2.0: version "1.2.1" resolved "http://registry.npm.taobao.org/formidable/download/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" +formstream@^1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/formstream/download/formstream-1.1.0.tgz#51f3970f26136eb0ad44304de4cebb50207b4479" + dependencies: + destroy "^1.0.4" + mime "^1.3.4" + pause-stream "~0.0.11" + fragment-cache@^0.2.1: version "0.2.1" resolved "http://registry.npm.taobao.org/fragment-cache/download/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -5275,7 +5302,7 @@ moment-timezone@^0.5.0: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.19.2, moment@^2.19.3, moment@^2.22.2: +"moment@>= 2.9.0", moment@^2.19.2, moment@^2.19.3, moment@^2.22.1, moment@^2.22.2: version "2.22.2" resolved "http://registry.npm.taobao.org/moment/download/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" @@ -5966,7 +5993,7 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" -pause-stream@^0.0.11: +pause-stream@^0.0.11, pause-stream@~0.0.11: version "0.0.11" resolved "http://registry.npm.taobao.org/pause-stream/download/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" dependencies: @@ -6153,7 +6180,7 @@ prop-types@^15.6.2: loose-envify "^1.3.1" object-assign "^4.1.1" -proxy-agent@^2.1.0: +proxy-agent@^2.1.0, proxy-agent@^2.3.1: version "2.3.1" resolved "http://registry.npm.taobao.org/proxy-agent/download/proxy-agent-2.3.1.tgz#3d49d863d46cf5f37ca8394848346ea02373eac6" dependencies: @@ -7281,6 +7308,12 @@ tsscmp@1.0.5: version "1.0.5" resolved "http://registry.npm.taobao.org/tsscmp/download/tsscmp-1.0.5.tgz#7dc4a33af71581ab4337da91d85ca5427ebd9a97" +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "http://registry.npm.taobao.org/tunnel-agent/download/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + type-check@~0.3.2: version "0.3.2" resolved "http://registry.npm.taobao.org/type-check/download/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -7445,6 +7478,25 @@ urllib@^2.17.1, urllib@^2.24.0, urllib@^2.25.1, urllib@^2.29.1: statuses "^1.3.1" utility "^1.12.0" +urllib@^2.22.0: + version "2.30.0" + resolved "http://registry.npm.taobao.org/urllib/download/urllib-2.30.0.tgz#68208f030e096ebeaf213110dc58d542220148c3" + dependencies: + any-promise "^1.3.0" + content-type "^1.0.2" + debug "^2.6.9" + default-user-agent "^1.0.0" + digest-header "^0.0.1" + ee-first "~1.1.1" + humanize-ms "^1.2.0" + iconv-lite "^0.4.15" + ip "^1.1.5" + proxy-agent "^2.3.1" + pump "^3.0.0" + qs "^6.4.0" + statuses "^1.3.1" + utility "^1.12.0" + use@^3.1.0: version "3.1.1" resolved "http://registry.npm.taobao.org/use/download/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" From 234bbe3b866d174b436384fb81479faa371db3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 7 Oct 2018 00:43:53 +0800 Subject: [PATCH 187/208] [chore] release 2.0.0-alpha.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 178675f..4ed3db9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "2.0.0-alpha.4", + "version": "2.0.0-alpha.5", "description": "", "private": true, "dependencies": { From fd6357d3e181c8d66aa7329a96ecf1502d27187e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 7 Oct 2018 19:26:00 +0800 Subject: [PATCH 188/208] update: article list api sort by default createdAt -1 --- app/controller/article.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/controller/article.js b/app/controller/article.js index e80f9f2..2f97070 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -119,10 +119,6 @@ module.exports = class ArticleController extends Controller { query.state = 1 // 文章列表不需要content和state options.select = '-content -renderedContent -state' - options.sort = { - updatedAt: -1, - createdAt: -1 - } } else { // 排序 if (sortBy && order) { From 97f7af73e4994b489096f8200e845ef7420a1daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 7 Oct 2018 19:47:41 +0800 Subject: [PATCH 189/208] update: rename router namespace --- app/router/backend.js | 2 +- app/router/frontend.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/router/backend.js b/app/router/backend.js index f080b35..1b26cf8 100644 --- a/app/router/backend.js +++ b/app/router/backend.js @@ -1,5 +1,5 @@ module.exports = app => { - const backendRouter = app.router.namespace('/v2/backend') + const backendRouter = app.router.namespace('/backend') const { controller, middlewares } = app const auth = middlewares.auth(app) diff --git a/app/router/frontend.js b/app/router/frontend.js index 610a1b5..f8ee28d 100644 --- a/app/router/frontend.js +++ b/app/router/frontend.js @@ -1,5 +1,5 @@ module.exports = app => { - const fontendRouter = app.router.namespace('/v2') + const fontendRouter = app.router.namespace('') const { controller } = app // Article From 23053c9bd8c9c87fa5b0263a82a09d13c1fe63bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sun, 7 Oct 2018 19:48:01 +0800 Subject: [PATCH 190/208] [chore] release 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ed3db9..55502eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "2.0.0-alpha.5", + "version": "2.0.0", "description": "", "private": true, "dependencies": { From a937c5256f997558b71c09d65d17d9e98060a606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 9 Oct 2018 01:26:19 +0800 Subject: [PATCH 191/208] update: add hitokoto api and refactor ip lookup service --- app/controller/agent.js | 15 ++++++++++++ app/extend/context.js | 30 +++++++++++------------ app/router/frontend.js | 42 +++++++++++++++++--------------- app/service/agent.js | 41 +++++++++++++++++++++++++++++++ app/service/aliApi.js | 53 ----------------------------------------- app/service/comment.js | 2 +- package.json | 1 + yarn.lock | 15 +++++++++++- 8 files changed, 109 insertions(+), 90 deletions(-) create mode 100644 app/controller/agent.js create mode 100644 app/service/agent.js delete mode 100644 app/service/aliApi.js diff --git a/app/controller/agent.js b/app/controller/agent.js new file mode 100644 index 0000000..bc8954f --- /dev/null +++ b/app/controller/agent.js @@ -0,0 +1,15 @@ +const { Controller } = require('egg') + +module.exports = class AgentController extends Controller { + async hitokoto () { + this.ctx.success(await this.service.agent.hitokoto()) + } + + async ip () { + const { ctx } = this + ctx.validate({ + ip: { type: 'string', required: true } + }, ctx.query) + this.ctx.success(await this.service.agent.lookupIp(ctx.query.ip), 'IP查询成功') + } +} diff --git a/app/extend/context.js b/app/extend/context.js index 6a41380..6cd4fb9 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -53,22 +53,20 @@ module.exports = { this.throw(422, '发布人不存在') } }, - async getLocation () { + async getCtxIp () { const req = this.req - const ip = (req.headers['x-forwarded-for'] || - req.headers['x-real-ip'] || - req.connection.remoteAddress || - req.socket.remoteAddress || - req.connection.socket.remoteAddress || - req.ip || - req.ips[0] || '').replace('::ffff:', '') - let location = {} - const { success, data } = await this.service.aliApi.lookupIp(ip) - if (success) { - location = data - } else { - location = geoip.lookup(ip) || {} - } - return { ip, location } + return (req.headers['x-forwarded-for'] + || req.headers['x-real-ip'] + || req.connection.remoteAddress + || req.socket.remoteAddress + || req.connection.socket.remoteAddress + || req.ip + || req.ips[0] + || '' + ).replace('::ffff:', '') + }, + async getLocation () { + const ip = this.getCtxIp() + return await this.service.agent.lookupIp(ip) } } diff --git a/app/router/frontend.js b/app/router/frontend.js index f8ee28d..e4382a0 100644 --- a/app/router/frontend.js +++ b/app/router/frontend.js @@ -1,34 +1,38 @@ module.exports = app => { - const fontendRouter = app.router.namespace('') + const frontendRouter = app.router.namespace('') const { controller } = app // Article - fontendRouter.get('/articles', controller.article.list) - fontendRouter.get('/articles/archives', controller.article.archives) - fontendRouter.get('/articles/hot', controller.article.hot) - fontendRouter.get('/articles/:id', controller.article.item) - fontendRouter.patch('/articles/:id', controller.article.like) - fontendRouter.patch('/articles/:id/like', controller.article.like) - fontendRouter.patch('/articles/:id/unlike', controller.article.unlike) + frontendRouter.get('/articles', controller.article.list) + frontendRouter.get('/articles/archives', controller.article.archives) + frontendRouter.get('/articles/hot', controller.article.hot) + frontendRouter.get('/articles/:id', controller.article.item) + frontendRouter.patch('/articles/:id', controller.article.like) + frontendRouter.patch('/articles/:id/like', controller.article.like) + frontendRouter.patch('/articles/:id/unlike', controller.article.unlike) // Category - fontendRouter.get('/categories', controller.category.list) - fontendRouter.get('/categories/:id', controller.category.item) + frontendRouter.get('/categories', controller.category.list) + frontendRouter.get('/categories/:id', controller.category.item) // Tag - fontendRouter.get('/tags', controller.tag.list) - fontendRouter.get('/tags/:id', controller.tag.item) + frontendRouter.get('/tags', controller.tag.list) + frontendRouter.get('/tags/:id', controller.tag.item) // Comment - fontendRouter.get('/comments', controller.comment.list) - fontendRouter.get('/comments/:id', controller.comment.item) - fontendRouter.post('/comments', controller.comment.create) - fontendRouter.patch('/comments/:id/like', controller.comment.like) - fontendRouter.patch('/comments/:id/unlike', controller.comment.unlike) + frontendRouter.get('/comments', controller.comment.list) + frontendRouter.get('/comments/:id', controller.comment.item) + frontendRouter.post('/comments', controller.comment.create) + frontendRouter.patch('/comments/:id/like', controller.comment.like) + frontendRouter.patch('/comments/:id/unlike', controller.comment.unlike) // User - fontendRouter.get('/users/:id', controller.user.item) + frontendRouter.get('/users/:id', controller.user.item) // Setting - fontendRouter.get('/setting', controller.setting.index) + frontendRouter.get('/setting', controller.setting.index) + + // Agent + frontendRouter.get('/agent/hitokoto', controller.agent.hitokoto) + frontendRouter.get('/agent/ip', controller.agent.ip) } diff --git a/app/service/agent.js b/app/service/agent.js new file mode 100644 index 0000000..e02700e --- /dev/null +++ b/app/service/agent.js @@ -0,0 +1,41 @@ +/** + * @desc api 代理 + */ + +const axios = require('axios') +const geoip = require('geoip-lite') +const { Service } = require('egg') + +module.exports = class AgentService extends Service { + async lookupIp (ip) { + ip = ip || this.ctx.getCtxIp() + const res = await axios.get('https://dm-81.data.aliyun.com/rest/160601/ip/getIpInfo.json', { + headers: { + Authorization: `APPCODE ${this.app.setting.keys.aliApiGateway.ip.appCode}` + }, + params: { + ip + } + }).catch(() => null) + let location = {} + if (res && res.status === 200 && !res.data.code) { + location = res.data.data + } else { + location = geoip.lookup(ip) || {} + } + return { + ip, + location + } + } + + async hitokoto () { + const res = await axios.get('https://api.lwl12.com/hitokoto/main/get', { + params: { + encode: 'realjson', + charset: 'utf-8' + } + }) + return res.status === 200 ? res.data : null + } +} diff --git a/app/service/aliApi.js b/app/service/aliApi.js deleted file mode 100644 index 5a84c54..0000000 --- a/app/service/aliApi.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @desc 阿里Api市场服务 Services - */ - -const https = require('https') -const { Service } = require('egg') - -module.exports = class AliApiService extends Service { - lookupIp (ip) { - return new Promise(resolve => { - const req = https.request({ - hostname: 'dm-81.data.aliyun.com', - port: 443, - path: `/rest/160601/ip/getIpInfo.json?ip=${ip}`, - method: 'GET', - protocol: 'https:', - headers: { - Authorization: `APPCODE ${this.app.setting.keys.aliApiGateway.ip.appCode}` - } - }, res => { - let success = false - let data = null - if (res.statusCode === 200) { - success = true - } - res.on('data', d => { - data = JSON.parse(d) - }) - res.on('end', () => { - if (success && data && !data.code) { - this.app.logger.info('IP地址查询成功,ip:' + ip) - return resolve({ - success: true, - data: data.data - }) - } - resolve({ - success: false, - data - }) - }) - }) - req.on('error', err => { - this.app.logger.error(err) - resolve({ - success: false, - data: err - }) - }) - req.end() - }) - } -} diff --git a/app/service/comment.js b/app/service/comment.js index 0efb542..93ad97f 100644 --- a/app/service/comment.js +++ b/app/service/comment.js @@ -99,7 +99,7 @@ module.exports = class CommentService extends ProxyService { const url = this.config.author.url switch (type) { case COMMENT: - return `${url}/articles/${article._id || article}` + return `${url}/article/${article._id || article}` case MESSAGE: return `${url}/guestbook` default: diff --git a/package.json b/package.json index 55502eb..82263d6 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "private": true, "dependencies": { "akismet-api": "^4.2.0", + "axios": "^0.18.0", "bcryptjs": "^2.4.3", "egg": "^2.2.1", "egg-alinode-async": "^2.1.2", diff --git a/yarn.lock b/yarn.lock index a6365f0..5f92bd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -641,6 +641,13 @@ await-first@^1.0.0: dependencies: ee-first "^1.1.1" +axios@^0.18.0: + version "0.18.0" + resolved "http://registry.npm.taobao.org/axios/download/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" + dependencies: + follow-redirects "^1.3.0" + is-buffer "^1.1.5" + axobject-query@^2.0.1: version "2.0.1" resolved "http://registry.npm.taobao.org/axobject-query/download/axobject-query-2.0.1.tgz#05dfa705ada8ad9db993fa6896f22d395b0b0a07" @@ -2300,7 +2307,7 @@ debug@2, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.1, d dependencies: ms "2.0.0" -debug@3.1.0, debug@~3.1.0: +debug@3.1.0, debug@=3.1.0, debug@~3.1.0: version "3.1.0" resolved "http://registry.npm.taobao.org/debug/download/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -3498,6 +3505,12 @@ flexbuffer@0.0.6: version "0.0.6" resolved "http://registry.npm.taobao.org/flexbuffer/download/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30" +follow-redirects@^1.3.0: + version "1.5.8" + resolved "http://registry.npm.taobao.org/follow-redirects/download/follow-redirects-1.5.8.tgz#1dbfe13e45ad969f813e86c00e5296f525c885a1" + dependencies: + debug "=3.1.0" + for-in@^1.0.2: version "1.0.2" resolved "http://registry.npm.taobao.org/for-in/download/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" From 009ce51da6df66200db375a765dc2281da17fd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 9 Oct 2018 13:04:43 +0800 Subject: [PATCH 192/208] fix: get ctx ip error:wqw --- app/extend/context.js | 2 +- app/middleware/error.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/extend/context.js b/app/extend/context.js index 6cd4fb9..0e80ed0 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -53,7 +53,7 @@ module.exports = { this.throw(422, '发布人不存在') } }, - async getCtxIp () { + getCtxIp () { const req = this.req return (req.headers['x-forwarded-for'] || req.headers['x-real-ip'] diff --git a/app/middleware/error.js b/app/middleware/error.js index ac9b292..7c1a9d8 100644 --- a/app/middleware/error.js +++ b/app/middleware/error.js @@ -9,6 +9,7 @@ module.exports = (opt, app) => { } catch (err) { // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 ctx.app.emit('error', err, ctx) + app.logger.error(err) let code = err.status || 500 if (code === 200) code = -1 let message = '' From 3bde2d6d21cdd4065e0135bfaacc657311ff637b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 9 Oct 2018 13:20:04 +0800 Subject: [PATCH 193/208] update: change voice --- app/controller/agent.js | 4 ++-- app/router/frontend.js | 2 +- app/service/agent.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controller/agent.js b/app/controller/agent.js index bc8954f..11f444c 100644 --- a/app/controller/agent.js +++ b/app/controller/agent.js @@ -1,8 +1,8 @@ const { Controller } = require('egg') module.exports = class AgentController extends Controller { - async hitokoto () { - this.ctx.success(await this.service.agent.hitokoto()) + async voice () { + this.ctx.success(await this.service.agent.voice()) } async ip () { diff --git a/app/router/frontend.js b/app/router/frontend.js index e4382a0..d281480 100644 --- a/app/router/frontend.js +++ b/app/router/frontend.js @@ -33,6 +33,6 @@ module.exports = app => { frontendRouter.get('/setting', controller.setting.index) // Agent - frontendRouter.get('/agent/hitokoto', controller.agent.hitokoto) + frontendRouter.get('/agent/voice', controller.agent.voice) frontendRouter.get('/agent/ip', controller.agent.ip) } diff --git a/app/service/agent.js b/app/service/agent.js index e02700e..eaa3195 100644 --- a/app/service/agent.js +++ b/app/service/agent.js @@ -29,7 +29,7 @@ module.exports = class AgentService extends Service { } } - async hitokoto () { + async voice () { const res = await axios.get('https://api.lwl12.com/hitokoto/main/get', { params: { encode: 'realjson', From 7e1ffc099d23f961f566dc2aa50f2092440b2152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 9 Oct 2018 22:56:30 +0800 Subject: [PATCH 194/208] update: add docker volume for egg logs --- app/schedule/links.js | 2 +- app/schedule/personal.js | 4 ++-- app/service/setting.js | 2 +- docker-compose.yml | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/schedule/links.js b/app/schedule/links.js index 1431abf..e8bd1fc 100644 --- a/app/schedule/links.js +++ b/app/schedule/links.js @@ -9,7 +9,7 @@ module.exports = class Links extends Subscription { return { // 每天0点更新一次 cron: '0 0 * * * *', - type: 'all' + type: 'worker' } } diff --git a/app/schedule/personal.js b/app/schedule/personal.js index 612897c..554f5ca 100644 --- a/app/schedule/personal.js +++ b/app/schedule/personal.js @@ -8,8 +8,8 @@ module.exports = class Links extends Subscription { static get schedule () { return { // 每小时更新一次 - cron: '0 0 */1 * * *', - type: 'all' + interval: '1h', + type: 'worker' } } diff --git a/app/service/setting.js b/app/service/setting.js index 0850866..0d138ab 100644 --- a/app/service/setting.js +++ b/app/service/setting.js @@ -95,7 +95,7 @@ module.exports = class SettingService extends ProxyService { } }) // 个人github信息更新成功 - this.logger.info('个人github信息更新成功') + this.logger.info('个人GitHub信息更新成功') this.mountToApp(setting) return setting } diff --git a/docker-compose.yml b/docker-compose.yml index 242e3e8..c63278f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,8 @@ services: depends_on: - redis - mongodb + volumes: + - /root/logs networks: - docker-node-server # 端口映射 From f9a89c5b8ba98592bbc482fc086f8663dee7a0e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Tue, 9 Oct 2018 22:58:03 +0800 Subject: [PATCH 195/208] [chore] release 2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 82263d6..5c67dd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "2.0.0", + "version": "2.0.1", "description": "", "private": true, "dependencies": { From 178a6ebaa35d51cbb1e814162de0a6965e10cb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 10 Oct 2018 16:21:22 +0800 Subject: [PATCH 196/208] chore: update README and add CHANGELOG --- CHANGELOG.md | 55 +++++++++++++++++++++++++++++++ README.md | 88 +++++++++++++++++++++++++++++++++++++++++++++---- README.zh-CN.md | 39 ---------------------- 3 files changed, 136 insertions(+), 46 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 README.zh-CN.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4333667 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# CHANGELOG + +## v2.0.1 + +2018-10-09 + +* fix: 获取context的ip错误 +* chore: docker添加logs的volume + +## v2.0.0 + +2018-10-07 + +* 框架:用Egg重构 +* Model层 + - article增加原创、转载字段 + - 新增notification站内通知和stat站内统计模型 + - user简化,去掉不必要字段 + - setting重构,分类型 +* 接口 + - 新增voice接口获取一些心灵鸡汤文字 + - 新增ip接口查询ip +* 服务 + - ip查询优先阿里云IP查询,geoip-lite为降级 + - 定时任务换成egg的schedule + - model proxy重构 + - 业务逻辑拆分,每个model都有其对应的service层 + - admin user和setting初始化流程变更 + - 完善的日志系统 +* addon + - 接入sentry + - docker支持 + - 增加release tag + + +## v1.1.0 + +* 文章归档api(2018.01.04) +* Model代理 (2018.01.28) +* ESlint (2018.02.01 + +## v1.0.0 + +* 音乐api (2017.9.26) +* Github oauth 代理 (2017.9.28) +* 文章分类api (2017.10.26) +* Redis缓存部分数据 (2017.10.27 v1.1) +* 评论api (2017.10.28) +* 评论定位 [geoip](https://github.com/bluesmoon/node-geoip) (2017.10.29) +* 垃圾评论过滤 [akismet](https://github.com/chrisfosterelli/akismet-api) (2017.10.29) +* 用户禁言 (2017.10.29) +* 评论发送邮件 [nodemailer](https://github.com/nodemailer/nodemailer) (2017.10.29) +* GC优化 (2017.10.30,linux下需要预先安装g++, **已废弃**) +* 个人动态api (2017.10.30) + diff --git a/README.md b/README.md index 9e4064f..2fa1c8e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,41 @@ +[C-CLIENT]: https://jooger.me +[S-CLIENT]: https://api.jooger.me +[egg]: https://eggjs.org +[egg-image]: https://img.shields.io/badge/Powered%20By-Egg.js-ff69b4.svg?style=flat-square +[david-image]: https://img.shields.io/david/jo0ger/node-server.svg?style=flat-square +[david-url]: https://david-dm.org/jo0ger/node-server + # node-server +[![powered by Egg.js][egg-image]][egg] +[![David deps][david-image]][david-url] +[![GitHub forks](https://img.shields.io/github/forks/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/network) +[![GitHub stars](https://img.shields.io/github/stars/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/stargazers) +[![GitHub issues](https://img.shields.io/github/issues/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/issues) +[![GitHub last commit](https://img.shields.io/github/last-commit/jo0ger/node-server.svg?style=flat-square)](https://github.com/jo0ger/node-server/commits/master) + +RESTful API server application for my blog +* Web client for user: [jooger.me]([C-CLIENT]) powered by [Nuxt.js@2](https://github.com/nuxt/nuxt.js) and [TypeScript](https://github.com/Microsoft/TypeScript) +* Web client for admin: vue-admin powered by Vue and iview +* Server client: [api.jooger.me]([S-CLIENT]) powered by [Egg](https://github.com/eggjs/egg) and mongodb -## QuickStart +## Quick Start - +### Environment Dependencies -see [egg docs][egg] for more detail. +- [redis](https://redis.io/) +- [mongodb](https://www.mongodb.com/) ### Development -```bash -$ npm i -$ npm run dev +Please make sure they are configured the same as `config/config.default.js` + +``` bash +$ yarn + +$ yarn dev + $ open http://localhost:7001/ ``` @@ -29,5 +52,56 @@ $ npm stop - Use `npm test` to run unit test. - Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail. +### Develop / Deploy with Docker + +#### Requirements + +* docker +* docker-compose + +#### Config + +##### docker-compose config + +* development: docker-compose.dev.yml +* production: docker-compose.yml + +##### Change port + +``` yml +version: "3" +services: + node-server: + ports: + - ${HOST PORT}:7001 +``` + +#### Develop + +``` bash +# start +$ docker-compose -f docker-compose.dev.yml up + +# stop +$ docker-compose -f docker-compose.dev.yml down + +# stop and remove valume/cache +$ docker-compose -f docker-compose.dev.yml down -v +``` + +#### Deploy + +``` bash +# start +$ docker-compose up -d + +# stop +$ docker-compose down + +# stop and remove volume/cache +$ docker-compose down -v +``` + +## CHANGELOG -[egg]: https://eggjs.org \ No newline at end of file +[HERE](CHANGELOG.md) diff --git a/README.zh-CN.md b/README.zh-CN.md deleted file mode 100644 index dea4ee8..0000000 --- a/README.zh-CN.md +++ /dev/null @@ -1,39 +0,0 @@ -# node-server - - - -## 快速入门 - - - -如需进一步了解,参见 [egg 文档][egg]。 - -### 本地开发 - -```bash -$ npm i -$ npm run dev -$ open http://localhost:7001/ -``` - -### 部署 - -```bash -$ npm start -$ npm stop -``` - -### 单元测试 - -- [egg-bin] 内置了 [mocha], [thunk-mocha], [power-assert], [istanbul] 等框架,让你可以专注于写单元测试,无需理会配套工具。 -- 断言库非常推荐使用 [power-assert]。 -- 具体参见 [egg 文档 - 单元测试](https://eggjs.org/zh-cn/core/unittest)。 - -### 内置指令 - -- 使用 `npm run lint` 来做代码风格检查。 -- 使用 `npm test` 来执行单元测试。 -- 使用 `npm run autod` 来自动检测依赖更新,详细参见 [autod](https://www.npmjs.com/package/autod) 。 - - -[egg]: https://eggjs.org From 79ea5bcd84aa3b33e38a83ff58f7ce5dd2f792d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 10 Oct 2018 22:21:02 +0800 Subject: [PATCH 197/208] update: related article sort by -createdAt --- app/service/article.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/service/article.js b/app/service/article.js index 20f6224..cbb4a37 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -120,7 +120,9 @@ module.exports = class ArticleService extends ProxyService { tag: { $in: tag.map(t => t._id) } }, 'title thumb createdAt publishedAt meta category', - null, + { + sort: '-createdAt' + }, { path: 'category', select: 'name description' From aaebfbfd6d3d99e66767a3ff078ad5c911abf0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 10 Oct 2018 22:25:13 +0800 Subject: [PATCH 198/208] chore: add pre-commit hook to ling the code --- app/extend/application.js | 1 - app/extend/context.js | 2 - package.json | 21 +- yarn.lock | 455 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 450 insertions(+), 29 deletions(-) diff --git a/app/extend/application.js b/app/extend/application.js index 1fda584..22a0b62 100644 --- a/app/extend/application.js +++ b/app/extend/application.js @@ -1,7 +1,6 @@ const mongoosePaginate = require('mongoose-paginate-v2') const lodash = require('lodash') const merge = require('merge') -const { isEmptyObject } = require('../utils/validate') const prefix = 'http://' diff --git a/app/extend/context.js b/app/extend/context.js index 0e80ed0..ac0f1d0 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -1,5 +1,3 @@ -const geoip = require('geoip-lite') - module.exports = { processPayload (payload) { if (!payload) return null diff --git a/package.json b/package.json index 5c67dd4..967e261 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "egg-mock": "^3.14.0", "eslint": "^4.11.0", "eslint-config-egg": "^6.0.0", + "pre-git": "^3.17.1", "release-it": "^7.6.1", "webstorm-disable-index": "^1.2.0" }, @@ -60,7 +61,8 @@ "lint": "eslint . --fix", "ci": "npm run lint && npm run cov", "autod": "autod", - "rc": "release-it" + "rc": "release-it", + "commit": "commit-wizard" }, "ci": { "version": "8" @@ -74,5 +76,20 @@ "email": "iamjooger@gmail.com", "url": "https://jooger.me" }, - "license": "MIT" + "license": "MIT", + "release": { + "analyzeCommits": "simple-commit-message" + }, + "config": { + "pre-git": { + "commit-msg": "simple", + "pre-commit": [ + "yarn lint" + ], + "pre-push": [], + "post-commit": [], + "post-checkout": [], + "post-merge": [] + } + } } diff --git a/yarn.lock b/yarn.lock index 5f92bd5..d5fad42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -434,6 +434,14 @@ ali-oss@^4.10.1: utility "^1.8.0" xml2js "^0.4.16" +always-error@1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/always-error/download/always-error-1.0.0.tgz#95c84042cfa86f38c86ca6c2cc42c0a0103441b2" + +am-i-a-dependency@1.1.2: + version "1.1.2" + resolved "http://registry.npm.taobao.org/am-i-a-dependency/download/am-i-a-dependency-1.1.2.tgz#f9d3422304d6f642f821e4c407565035f6167f1f" + amdefine@>=0.0.4: version "1.0.1" resolved "http://registry.npm.taobao.org/amdefine/download/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -444,10 +452,18 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "http://registry.npm.taobao.org/ansi-escapes/download/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + ansi-escapes@^3.0.0: version "3.1.0" resolved "http://registry.npm.taobao.org/ansi-escapes/download/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" +ansi-regex@^1.0.0, ansi-regex@^1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-1.1.1.tgz#41c847194646375e6a1a5d10c3ca054ef9fc980d" + ansi-regex@^2.0.0: version "2.1.1" resolved "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -568,7 +584,7 @@ arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "http://registry.npm.taobao.org/arrify/download/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" -asap@^2.0.3: +asap@^2.0.0, asap@^2.0.3: version "2.0.6" resolved "http://registry.npm.taobao.org/asap/download/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -1382,11 +1398,15 @@ black-hole-stream@~0.0.1: version "0.0.1" resolved "http://registry.npm.taobao.org/black-hole-stream/download/black-hole-stream-0.0.1.tgz#33b7a06b9f1e7453d6041b82974481d2152aea42" +bluebird@2.9.24: + version "2.9.24" + resolved "http://registry.npm.taobao.org/bluebird/download/bluebird-2.9.24.tgz#14a2e75f0548323dc35aa440d92007ca154e967c" + bluebird@3.5.1: version "3.5.1" resolved "http://registry.npm.taobao.org/bluebird/download/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" -bluebird@^3.1.1, bluebird@^3.3.4, bluebird@^3.5.0: +bluebird@^3.1.1, bluebird@^3.3.4, bluebird@^3.5.0, bluebird@^3.5.1: version "3.5.2" resolved "http://registry.npm.taobao.org/bluebird/download/bluebird-3.5.2.tgz#1be0908e054a751754549c270489c1505d4ab15a" @@ -1633,6 +1653,14 @@ cfork@^1.6.1, cfork@^1.7.1: dependencies: utility "^1.12.0" +chalk@2.3.2: + version "2.3.2" + resolved "http://registry.npm.taobao.org/chalk/download/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.1: version "2.4.1" resolved "http://registry.npm.taobao.org/chalk/download/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" @@ -1641,7 +1669,7 @@ chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4. escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "http://registry.npm.taobao.org/chalk/download/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -1690,6 +1718,27 @@ charenc@~0.0.1: version "0.0.2" resolved "http://registry.npm.taobao.org/charenc/download/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" +chdir-promise@0.6.2: + version "0.6.2" + resolved "http://registry.npm.taobao.org/chdir-promise/download/chdir-promise-0.6.2.tgz#d4cfa0a96a112a8149341b69e2866d162f0e2dbd" + dependencies: + bluebird "^3.5.1" + check-more-types "2.24.0" + debug "3.1.0" + lazy-ass "1.6.0" + +check-more-types@2.23.0: + version "2.23.0" + resolved "http://registry.npm.taobao.org/check-more-types/download/check-more-types-2.23.0.tgz#6226264d30b1095aa1c0a5b874edbdd5d2d0a66f" + +check-more-types@2.24.0: + version "2.24.0" + resolved "http://registry.npm.taobao.org/check-more-types/download/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" + +check-more-types@2.3.0: + version "2.3.0" + resolved "http://registry.npm.taobao.org/check-more-types/download/check-more-types-2.3.0.tgz#b8397c69dc92a3e645f18932c045b09c74419ec4" + chokidar@^2.0.0: version "2.0.4" resolved "http://registry.npm.taobao.org/chokidar/download/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" @@ -1738,6 +1787,12 @@ cli-boxes@^1.0.0: version "1.0.0" resolved "http://registry.npm.taobao.org/cli-boxes/download/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" +cli-cursor@^1.0.1: + version "1.0.2" + resolved "http://registry.npm.taobao.org/cli-cursor/download/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + cli-cursor@^2.1.0: version "2.1.0" resolved "http://registry.npm.taobao.org/cli-cursor/download/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -1748,6 +1803,16 @@ cli-spinners@^1.1.0: version "1.3.1" resolved "http://registry.npm.taobao.org/cli-spinners/download/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a" +cli-table@0.3.1: + version "0.3.1" + resolved "http://registry.npm.taobao.org/cli-table/download/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" + dependencies: + colors "1.0.3" + +cli-width@^1.0.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/cli-width/download/cli-width-1.1.1.tgz#a4d293ef67ebb7b88d4a4d42c0ccf00c4d1e366d" + cli-width@^2.0.0: version "2.2.0" resolved "http://registry.npm.taobao.org/cli-width/download/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" @@ -1878,16 +1943,32 @@ color-name@1.1.3: version "1.1.3" resolved "http://registry.npm.taobao.org/color-name/download/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" +colors@1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/colors/download/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +colors@1.1.2: + version "1.1.2" + resolved "http://registry.npm.taobao.org/colors/download/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + colors@^1.1.2: version "1.3.2" resolved "http://registry.npm.taobao.org/colors/download/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b" +colors@~0.6.0-1: + version "0.6.2" + resolved "http://registry.npm.taobao.org/colors/download/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc" + combined-stream@1.0.6: version "1.0.6" resolved "http://registry.npm.taobao.org/combined-stream/download/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" dependencies: delayed-stream "~1.0.0" +commander@2.12.2: + version "2.12.2" + resolved "http://registry.npm.taobao.org/commander/download/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" + commander@2.15.1: version "2.15.1" resolved "http://registry.npm.taobao.org/commander/download/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" @@ -1896,6 +1977,10 @@ commander@^2.11.0, commander@^2.9.0: version "2.18.0" resolved "http://registry.npm.taobao.org/commander/download/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" +commander@~2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/commander/download/commander-2.1.0.tgz#d121bbae860d9992a3d517ba96f56588e47c6781" + commander@~2.17.1: version "2.17.1" resolved "http://registry.npm.taobao.org/commander/download/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -2099,6 +2184,19 @@ conventional-changelog@2.0.3: conventional-changelog-jshint "^2.0.0" conventional-changelog-preset-loader "^2.0.1" +conventional-commit-message@1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/conventional-commit-message/download/conventional-commit-message-1.1.0.tgz#ece8c661a168e983692e1d5a14875acb59510f6a" + dependencies: + check-more-types "2.3.0" + cz-conventional-changelog "1.1.5" + lazy-ass "1.3.0" + word-wrap "1.1.0" + +conventional-commit-types@^2.0.0: + version "2.2.0" + resolved "http://registry.npm.taobao.org/conventional-commit-types/download/conventional-commit-types-2.2.0.tgz#5db95739d6c212acbe7b6f656a11b940baa68946" + conventional-commits-filter@^2.0.0: version "2.0.0" resolved "http://registry.npm.taobao.org/conventional-commits-filter/download/conventional-commits-filter-2.0.0.tgz#a0ce1d1ff7a1dd7fab36bee8e8256d348d135651" @@ -2261,6 +2359,26 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +cz-conventional-changelog@1.1.5: + version "1.1.5" + resolved "http://registry.npm.taobao.org/cz-conventional-changelog/download/cz-conventional-changelog-1.1.5.tgz#0a4d1550c4e2fb6a3aed8f6cd858c21760e119b8" + dependencies: + word-wrap "^1.0.3" + +cz-conventional-changelog@2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/cz-conventional-changelog/download/cz-conventional-changelog-2.1.0.tgz#2f4bc7390e3244e4df293e6ba351e4c740a7c764" + dependencies: + conventional-commit-types "^2.0.0" + lodash.map "^4.5.1" + longest "^1.0.1" + right-pad "^1.0.1" + word-wrap "^1.0.3" + +d3-helpers@0.3.0: + version "0.3.0" + resolved "http://registry.npm.taobao.org/d3-helpers/download/d3-helpers-0.3.0.tgz#4b31dce4a2121a77336384574d893fbed5fb293d" + d@1: version "1.0.0" resolved "http://registry.npm.taobao.org/d/download/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" @@ -3344,6 +3462,10 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +exit-hook@^1.0.0: + version "1.1.1" + resolved "http://registry.npm.taobao.org/exit-hook/download/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + expand-brackets@^2.1.4: version "2.1.4" resolved "http://registry.npm.taobao.org/expand-brackets/download/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -3435,6 +3557,13 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +figures@^1.3.5: + version "1.7.0" + resolved "http://registry.npm.taobao.org/figures/download/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + figures@^2.0.0: version "2.0.0" resolved "http://registry.npm.taobao.org/figures/download/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -3469,6 +3598,16 @@ find-cache-dir@^2.0.0: make-dir "^1.0.0" pkg-dir "^3.0.0" +find-parent-dir@^0.3.0: + version "0.3.0" + resolved "http://registry.npm.taobao.org/find-parent-dir/download/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" + +find-up@2.1.0, find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "http://registry.npm.taobao.org/find-up/download/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + find-up@^1.0.0: version "1.1.2" resolved "http://registry.npm.taobao.org/find-up/download/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -3476,18 +3615,19 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" -find-up@^2.0.0, find-up@^2.1.0: - version "2.1.0" - resolved "http://registry.npm.taobao.org/find-up/download/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - dependencies: - locate-path "^2.0.0" - find-up@^3.0.0: version "3.0.0" resolved "http://registry.npm.taobao.org/find-up/download/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" dependencies: locate-path "^3.0.0" +findup@0.1.5: + version "0.1.5" + resolved "http://registry.npm.taobao.org/findup/download/findup-0.1.5.tgz#8ad929a3393bac627957a7e5de4623b06b0e2ceb" + dependencies: + colors "~0.6.0-1" + commander "~2.1.0" + flat-cache@^1.2.1: version "1.3.0" resolved "http://registry.npm.taobao.org/flat-cache/download/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" @@ -3673,6 +3813,32 @@ get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "http://registry.npm.taobao.org/get-value/download/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" +ggit@2.4.2: + version "2.4.2" + resolved "http://registry.npm.taobao.org/ggit/download/ggit-2.4.2.tgz#38fbddfffd2faab98e29f8a15e12d59a6557abbc" + dependencies: + always-error "1.0.0" + bluebird "3.5.1" + chdir-promise "0.6.2" + check-more-types "2.24.0" + cli-table "0.3.1" + colors "1.1.2" + commander "2.12.2" + d3-helpers "0.3.0" + debug "3.1.0" + find-up "2.1.0" + glob "7.1.2" + lazy-ass "1.6.0" + lodash "4.17.4" + moment "2.19.3" + moment-timezone "0.5.14" + optimist "0.6.1" + pluralize "7.0.0" + q "2.0.3" + quote "0.4.0" + ramda "0.25.0" + semver "5.4.1" + git-raw-commits@^2.0.0: version "2.0.0" resolved "http://registry.npm.taobao.org/git-raw-commits/download/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" @@ -3934,6 +4100,10 @@ hosted-git-info@^2.1.4: version "2.7.1" resolved "http://registry.npm.taobao.org/hosted-git-info/download/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" +hr@0.1.3: + version "0.1.3" + resolved "http://registry.npm.taobao.org/hr/download/hr-0.1.3.tgz#d9aa30f5929dabfd0b65ba395938a3e184dbcafe" + http-assert@^1.3.0: version "1.4.0" resolved "http://registry.npm.taobao.org/http-assert/download/http-assert-1.4.0.tgz#0e550b4fca6adf121bbeed83248c17e62f593a9a" @@ -4061,39 +4231,77 @@ ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: version "1.3.5" resolved "http://registry.npm.taobao.org/ini/download/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" -inquirer@6.2.0: - version "6.2.0" - resolved "http://registry.npm.taobao.org/inquirer/download/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" +inquirer-confirm@0.2.2: + version "0.2.2" + resolved "http://registry.npm.taobao.org/inquirer-confirm/download/inquirer-confirm-0.2.2.tgz#6f406d037bf9d9e455ef0f953929f357fe9a8848" + dependencies: + bluebird "2.9.24" + inquirer "0.8.2" + +inquirer@0.12.0: + version "0.12.0" + resolved "http://registry.npm.taobao.org/inquirer/download/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +inquirer@0.8.2: + version "0.8.2" + resolved "http://registry.npm.taobao.org/inquirer/download/inquirer-0.8.2.tgz#41586548e1c5d9b3f81df7325034baacab6f58ab" + dependencies: + ansi-regex "^1.1.1" + chalk "^1.0.0" + cli-width "^1.0.1" + figures "^1.3.5" + lodash "^3.3.1" + readline2 "^0.1.1" + rx "^2.4.3" + through "^2.3.6" + +inquirer@3.3.0, inquirer@^3.0.6: + version "3.3.0" + resolved "http://registry.npm.taobao.org/inquirer/download/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" dependencies: ansi-escapes "^3.0.0" chalk "^2.0.0" cli-cursor "^2.1.0" cli-width "^2.0.0" - external-editor "^3.0.0" + external-editor "^2.0.4" figures "^2.0.0" - lodash "^4.17.10" + lodash "^4.3.0" mute-stream "0.0.7" run-async "^2.2.0" - rxjs "^6.1.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" string-width "^2.1.0" strip-ansi "^4.0.0" through "^2.3.6" -inquirer@^3.0.6: - version "3.3.0" - resolved "http://registry.npm.taobao.org/inquirer/download/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" +inquirer@6.2.0: + version "6.2.0" + resolved "http://registry.npm.taobao.org/inquirer/download/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" dependencies: ansi-escapes "^3.0.0" chalk "^2.0.0" cli-cursor "^2.1.0" cli-width "^2.0.0" - external-editor "^2.0.4" + external-editor "^3.0.0" figures "^2.0.0" - lodash "^4.3.0" + lodash "^4.17.10" mute-stream "0.0.7" run-async "^2.2.0" - rx-lite "^4.0.8" - rx-lite-aggregates "^4.0.8" + rxjs "^6.1.0" string-width "^2.1.0" strip-ansi "^4.0.0" through "^2.3.6" @@ -4784,12 +4992,31 @@ koa@^2.5.2: type-is "^1.6.16" vary "^1.1.2" +largest-semantic-change@1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/largest-semantic-change/download/largest-semantic-change-1.1.0.tgz#47f5be0006aa344347d3e776951a03d5c692d2fd" + dependencies: + check-more-types "2.23.0" + lazy-ass "1.5.0" + latest-version@^3.0.0: version "3.1.0" resolved "http://registry.npm.taobao.org/latest-version/download/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" dependencies: package-json "^4.0.0" +lazy-ass@1.3.0: + version "1.3.0" + resolved "http://registry.npm.taobao.org/lazy-ass/download/lazy-ass-1.3.0.tgz#7d0d14eef3ec9702c6f30c60ea81f1a8d3f900fb" + +lazy-ass@1.5.0: + version "1.5.0" + resolved "http://registry.npm.taobao.org/lazy-ass/download/lazy-ass-1.5.0.tgz#ca15be243c7c475b8565cdbfa0f9c2f374f2a01d" + +lazy-ass@1.6.0: + version "1.6.0" + resolved "http://registry.npm.taobao.org/lazy-ass/download/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" + lazy@^1.0.11: version "1.0.11" resolved "http://registry.npm.taobao.org/lazy/download/lazy-1.0.11.tgz#daa068206282542c088288e975c297c1ae77b690" @@ -4933,6 +5160,10 @@ lodash.keys@^4.2.0: version "4.2.0" resolved "http://registry.npm.taobao.org/lodash.keys/download/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205" +lodash.map@^4.5.1: + version "4.6.0" + resolved "http://registry.npm.taobao.org/lodash.map/download/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" + lodash.max@^4.0.1: version "4.0.1" resolved "http://registry.npm.taobao.org/lodash.max/download/lodash.max-4.0.1.tgz#8735566c618b35a9f760520b487ae79658af136a" @@ -4994,6 +5225,14 @@ lodash@4.17.10: version "4.17.10" resolved "http://registry.npm.taobao.org/lodash/download/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" +lodash@4.17.4: + version "4.17.4" + resolved "http://registry.npm.taobao.org/lodash/download/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +lodash@^3.3.1: + version "3.10.1" + resolved "http://registry.npm.taobao.org/lodash/download/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0: version "4.17.11" resolved "http://registry.npm.taobao.org/lodash/download/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -5012,6 +5251,10 @@ long@^4.0.0: version "4.0.0" resolved "http://registry.npm.taobao.org/long/download/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" +longest@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/longest/download/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + loose-envify@^1.0.0, loose-envify@^1.3.1: version "1.4.0" resolved "http://registry.npm.taobao.org/loose-envify/download/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -5309,12 +5552,22 @@ modify-values@^1.0.0: version "1.0.1" resolved "http://registry.npm.taobao.org/modify-values/download/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" +moment-timezone@0.5.14: + version "0.5.14" + resolved "http://registry.npm.taobao.org/moment-timezone/download/moment-timezone-0.5.14.tgz#4eb38ff9538b80108ba467a458f3ed4268ccfcb1" + dependencies: + moment ">= 2.9.0" + moment-timezone@^0.5.0: version "0.5.21" resolved "http://registry.npm.taobao.org/moment-timezone/download/moment-timezone-0.5.21.tgz#3cba247d84492174dbf71de2a9848fa13207b845" dependencies: moment ">= 2.9.0" +moment@2.19.3: + version "2.19.3" + resolved "http://registry.npm.taobao.org/moment/download/moment-2.19.3.tgz#bdb99d270d6d7fda78cc0fbace855e27fe7da69f" + "moment@>= 2.9.0", moment@^2.19.2, moment@^2.19.3, moment@^2.22.1, moment@^2.22.2: version "2.22.2" resolved "http://registry.npm.taobao.org/moment/download/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" @@ -5455,6 +5708,14 @@ mustache@^2.3.0: version "2.3.2" resolved "http://registry.npm.taobao.org/mustache/download/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" +mute-stream@0.0.4: + version "0.0.4" + resolved "http://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.4.tgz#a9219960a6d5d5d046597aee51252c6655f7177e" + +mute-stream@0.0.5: + version "0.0.5" + resolved "http://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + mute-stream@0.0.7: version "0.0.7" resolved "http://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -5715,6 +5976,10 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +onetime@^1.0.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/onetime/download/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + onetime@^2.0.0: version "2.0.1" resolved "http://registry.npm.taobao.org/onetime/download/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" @@ -5725,7 +5990,7 @@ only@~0.0.2: version "0.0.2" resolved "http://registry.npm.taobao.org/only/download/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" -optimist@^0.6.1: +optimist@0.6.1, optimist@^0.6.1: version "0.6.1" resolved "http://registry.npm.taobao.org/optimist/download/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" dependencies: @@ -6054,10 +6319,14 @@ platform@^1.3.1, platform@^1.3.4: version "1.3.5" resolved "http://registry.npm.taobao.org/platform/download/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444" -pluralize@^7.0.0: +pluralize@7.0.0, pluralize@^7.0.0: version "7.0.0" resolved "http://registry.npm.taobao.org/pluralize/download/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" +pop-iterate@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/pop-iterate/download/pop-iterate-1.0.1.tgz#ceacfdab4abf353d7a0f2aaa2c1fc7b3f9413ba3" + posix-character-classes@^0.1.0: version "0.1.1" resolved "http://registry.npm.taobao.org/posix-character-classes/download/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -6154,6 +6423,25 @@ power-assert@^1.6.0, power-assert@^1.6.1: universal-deep-strict-equal "^1.2.1" xtend "^4.0.0" +pre-git@^3.17.1: + version "3.17.1" + resolved "http://registry.npm.taobao.org/pre-git/download/pre-git-3.17.1.tgz#1950d40151e067f7bc11f65a554d259f1c0df261" + dependencies: + bluebird "3.5.1" + chalk "2.3.2" + check-more-types "2.24.0" + conventional-commit-message "1.1.0" + cz-conventional-changelog "2.1.0" + debug "3.1.0" + ggit "2.4.2" + inquirer "3.3.0" + lazy-ass "1.6.0" + require-relative "0.8.7" + shelljs "0.8.1" + simple-commit-message "4.0.3" + validate-commit-msg "2.14.0" + word-wrap "1.2.3" + prelude-ls@~1.1.2: version "1.1.2" resolved "http://registry.npm.taobao.org/prelude-ls/download/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -6236,6 +6524,14 @@ pumpify@^1.3.5: inherits "^2.0.3" pump "^2.0.0" +q@2.0.3: + version "2.0.3" + resolved "http://registry.npm.taobao.org/q/download/q-2.0.3.tgz#75b8db0255a1a5af82f58c3f3aaa1efec7d0d134" + dependencies: + asap "^2.0.0" + pop-iterate "^1.0.1" + weak-map "^1.0.5" + q@^1.4.1, q@^1.5.1: version "1.5.1" resolved "http://registry.npm.taobao.org/q/download/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -6260,6 +6556,14 @@ quick-lru@^1.0.0: version "1.1.0" resolved "http://registry.npm.taobao.org/quick-lru/download/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" +quote@0.4.0: + version "0.4.0" + resolved "http://registry.npm.taobao.org/quote/download/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01" + +ramda@0.25.0: + version "0.25.0" + resolved "http://registry.npm.taobao.org/ramda/download/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9" + random-bytes@~1.0.0: version "1.0.0" resolved "http://registry.npm.taobao.org/random-bytes/download/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" @@ -6373,6 +6677,21 @@ readdirp@^2.0.0: micromatch "^3.1.10" readable-stream "^2.0.2" +readline2@^0.1.1: + version "0.1.1" + resolved "http://registry.npm.taobao.org/readline2/download/readline2-0.1.1.tgz#99443ba6e83b830ef3051bfd7dc241a82728d568" + dependencies: + mute-stream "0.0.4" + strip-ansi "^2.0.1" + +readline2@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/readline2/download/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + ready-callback@^2.1.0: version "2.1.0" resolved "http://registry.npm.taobao.org/ready-callback/download/ready-callback-2.1.0.tgz#e382a9e33a568b8d771e04ef4ef0eb02d3dfa7e0" @@ -6536,6 +6855,10 @@ require-main-filename@^1.0.1: version "1.0.1" resolved "http://registry.npm.taobao.org/require-main-filename/download/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" +require-relative@0.8.7: + version "0.8.7" + resolved "http://registry.npm.taobao.org/require-relative/download/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de" + require-uncached@^1.0.3: version "1.0.3" resolved "http://registry.npm.taobao.org/require-uncached/download/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" @@ -6585,6 +6908,13 @@ responselike@1.0.2: dependencies: lowercase-keys "^1.0.0" +restore-cursor@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/restore-cursor/download/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + restore-cursor@^2.0.0: version "2.0.0" resolved "http://registry.npm.taobao.org/restore-cursor/download/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -6600,6 +6930,10 @@ retry@0.10.1: version "0.10.1" resolved "http://registry.npm.taobao.org/retry/download/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" +right-pad@^1.0.1: + version "1.0.1" + resolved "http://registry.npm.taobao.org/right-pad/download/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0" + rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.6.1, rimraf@^2.6.2: version "2.6.2" resolved "http://registry.npm.taobao.org/rimraf/download/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" @@ -6610,6 +6944,12 @@ rndm@1.2.0: version "1.2.0" resolved "http://registry.npm.taobao.org/rndm/download/rndm-1.2.0.tgz#f33fe9cfb52bbfd520aa18323bc65db110a1b76c" +run-async@^0.1.0: + version "0.1.0" + resolved "http://registry.npm.taobao.org/run-async/download/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + run-async@^2.2.0: version "2.3.0" resolved "http://registry.npm.taobao.org/run-async/download/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -6633,6 +6973,14 @@ rx-lite@*, rx-lite@^4.0.8: version "4.0.8" resolved "http://registry.npm.taobao.org/rx-lite/download/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" +rx-lite@^3.1.2: + version "3.1.2" + resolved "http://registry.npm.taobao.org/rx-lite/download/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +rx@^2.4.3: + version "2.5.3" + resolved "http://registry.npm.taobao.org/rx/download/rx-2.5.3.tgz#21adc7d80f02002af50dae97fd9dbf248755f566" + rxjs@^6.1.0: version "6.3.2" resolved "http://registry.npm.taobao.org/rxjs/download/rxjs-6.3.2.tgz#6a688b16c4e6e980e62ea805ec30648e1c60907f" @@ -6705,6 +7053,10 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" +semver-regex@1.0.0: + version "1.0.0" + resolved "http://registry.npm.taobao.org/semver-regex/download/semver-regex-1.0.0.tgz#92a4969065f9c70c694753d55248fc68f8f652c9" + "semver@2 || 3 || 4 || 5", semver@5.5.1, semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: version "5.5.1" resolved "http://registry.npm.taobao.org/semver/download/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" @@ -6713,6 +7065,10 @@ semver@5.4.1: version "5.4.1" resolved "http://registry.npm.taobao.org/semver/download/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" +semver@5.5.0: + version "5.5.0" + resolved "http://registry.npm.taobao.org/semver/download/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + sendmessage@^1.1.0: version "1.1.0" resolved "http://registry.npm.taobao.org/sendmessage/download/sendmessage-1.1.0.tgz#10a245cee2d50c759f1e09a23477b91496d09e35" @@ -6768,6 +7124,14 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "http://registry.npm.taobao.org/shebang-regex/download/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" +shelljs@0.8.1: + version "0.8.1" + resolved "http://registry.npm.taobao.org/shelljs/download/shelljs-0.8.1.tgz#729e038c413a2254c4078b95ed46e0397154a9f1" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + shelljs@0.8.2: version "0.8.2" resolved "http://registry.npm.taobao.org/shelljs/download/shelljs-0.8.2.tgz#345b7df7763f4c2340d584abb532c5f752ca9e35" @@ -6780,6 +7144,22 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "http://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +simple-commit-message@4.0.3: + version "4.0.3" + resolved "http://registry.npm.taobao.org/simple-commit-message/download/simple-commit-message-4.0.3.tgz#947a37834ef11611d4faea48158afe67eec981a6" + dependencies: + am-i-a-dependency "1.1.2" + check-more-types "2.24.0" + debug "3.1.0" + ggit "2.4.2" + hr "0.1.3" + inquirer "0.12.0" + inquirer-confirm "0.2.2" + largest-semantic-change "1.1.0" + lazy-ass "1.6.0" + semver "5.5.0" + word-wrap "1.2.3" + slash@^1.0.0: version "1.0.0" resolved "http://registry.npm.taobao.org/slash/download/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -7042,6 +7422,12 @@ stringifier@^1.3.0: traverse "^0.6.6" type-name "^2.0.1" +strip-ansi@^2.0.1: + version "2.0.1" + resolved "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-2.0.1.tgz#df62c1aa94ed2f114e1d0f21fd1d50482b79a60e" + dependencies: + ansi-regex "^1.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -7538,6 +7924,15 @@ uuid@3.3.2, uuid@^3.0.1, uuid@^3.3.2: version "3.3.2" resolved "http://registry.npm.taobao.org/uuid/download/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" +validate-commit-msg@2.14.0: + version "2.14.0" + resolved "http://registry.npm.taobao.org/validate-commit-msg/download/validate-commit-msg-2.14.0.tgz#e5383691012cbb270dcc0bc2a4effebe14890eac" + dependencies: + conventional-commit-types "^2.0.0" + find-parent-dir "^0.3.0" + findup "0.1.5" + semver-regex "1.0.0" + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "http://registry.npm.taobao.org/validate-npm-package-license/download/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -7559,6 +7954,10 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +weak-map@^1.0.5: + version "1.0.5" + resolved "http://registry.npm.taobao.org/weak-map/download/weak-map-1.0.5.tgz#79691584d98607f5070bd3b70a40e6bb22e401eb" + webstorm-disable-index@^1.2.0: version "1.2.0" resolved "http://registry.npm.taobao.org/webstorm-disable-index/download/webstorm-disable-index-1.2.0.tgz#03d55abe05aff34a6af12b7bfddcaee145c64bd0" @@ -7612,6 +8011,14 @@ window-size@^0.1.4: version "0.1.4" resolved "http://registry.npm.taobao.org/window-size/download/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" +word-wrap@1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/word-wrap/download/word-wrap-1.1.0.tgz#356153d61d10610d600785c5d701288e0ae764a6" + +word-wrap@1.2.3, word-wrap@^1.0.3: + version "1.2.3" + resolved "http://registry.npm.taobao.org/word-wrap/download/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + wordwrap@~0.0.2: version "0.0.3" resolved "http://registry.npm.taobao.org/wordwrap/download/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" From 20c2b9bc0935be7471206b35bf78cf5f9d163e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 10 Oct 2018 23:01:07 +0800 Subject: [PATCH 199/208] fix: fix docker-compose volumes named error --- app/schedule/links.js | 2 +- app/schedule/personal.js | 5 +++-- docker-compose.yml | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/schedule/links.js b/app/schedule/links.js index e8bd1fc..a4ecdd9 100644 --- a/app/schedule/links.js +++ b/app/schedule/links.js @@ -4,7 +4,7 @@ const { Subscription } = require('egg') -module.exports = class Links extends Subscription { +module.exports = class UpdateSiteLinks extends Subscription { static get schedule () { return { // 每天0点更新一次 diff --git a/app/schedule/personal.js b/app/schedule/personal.js index 554f5ca..c9fb00b 100644 --- a/app/schedule/personal.js +++ b/app/schedule/personal.js @@ -4,12 +4,13 @@ const { Subscription } = require('egg') -module.exports = class Links extends Subscription { +module.exports = class UpdatePersonalGithubInfo extends Subscription { static get schedule () { return { // 每小时更新一次 interval: '1h', - type: 'worker' + type: 'worker', + immediate: true } } diff --git a/docker-compose.yml b/docker-compose.yml index c63278f..882d9e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: - redis - mongodb volumes: - - /root/logs + - /root/logs:/root/logs networks: - docker-node-server # 端口映射 From 9e2f295171b77714a9d41e2a9e8f4d9eccd2762b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Wed, 10 Oct 2018 23:15:56 +0800 Subject: [PATCH 200/208] fix: enable marked sanitize options to sanitize the HTML passed into markdownString --- app/utils/markdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/markdown.js b/app/utils/markdown.js index eabcc6c..95802ce 100644 --- a/app/utils/markdown.js +++ b/app/utils/markdown.js @@ -102,7 +102,7 @@ marked.setOptions({ renderer, gfm: true, pedantic: false, - sanitize: false, + sanitize: true, tables: true, breaks: true, headerIds: true, From 451211482380654e909e34eedcbe4b5f7422605d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Thu, 11 Oct 2018 02:02:42 +0800 Subject: [PATCH 201/208] fix: remove repetitive error log at global error catch function --- app/middleware/error.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/middleware/error.js b/app/middleware/error.js index 7c1a9d8..ac9b292 100644 --- a/app/middleware/error.js +++ b/app/middleware/error.js @@ -9,7 +9,6 @@ module.exports = (opt, app) => { } catch (err) { // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 ctx.app.emit('error', err, ctx) - app.logger.error(err) let code = err.status || 500 if (code === 200) code = -1 let message = '' From d46c1417bfdecd7f12be13b7ec49c3a9aca36633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 12 Oct 2018 02:28:59 +0800 Subject: [PATCH 202/208] feat: add baidu seo push --- app/controller/article.js | 33 ++++++++++++++++------- app/model/setting.js | 4 +++ app/service/seo.js | 57 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 app/service/seo.js diff --git a/app/controller/article.js b/app/controller/article.js index 2f97070..2fbe574 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -186,9 +186,14 @@ module.exports = class ArticleController extends Controller { return ctx.fail('文章名称重复') } const data = await this.service.article.create(body) - data - ? ctx.success(data, '文章创建成功') - : ctx.fail('文章创建失败') + if (data) { + ctx.success(data, '文章创建成功') + if (!this.config.isProd) { + this.service.seo.baiduSeo('push', `${this.config.author.url}/article/${data._id}`) + } + } else { + ctx.fail('文章创建失败') + } } async update () { @@ -218,18 +223,28 @@ module.exports = class ArticleController extends Controller { null, 'category tag' ) - data - ? ctx.success(data, '文章更新成功') - : ctx.fail('文章更新失败') + if (data) { + ctx.success(data, '文章更新成功') + if (!this.config.isProd) { + this.service.seo.baiduSeo('update', `${this.config.author.url}/article/${data._id}`) + } + } else { + ctx.fail('文章更新失败') + } } async delete () { const { ctx } = this const params = ctx.validateParamsObjectId() const data = await this.service.article.deleteItemById(params.id) - data - ? ctx.success('文章删除成功') - : ctx.fail('文章删除失败') + if (data) { + ctx.success(data, '文章删除成功') + if (!this.config.isProd) { + this.service.seo.baiduSeo('delete', `${this.config.author.url}/article/${data._id}`) + } + } else { + ctx.fail('文章删除失败') + } } async like () { diff --git a/app/model/setting.js b/app/model/setting.js index 12e92e5..7a854f6 100644 --- a/app/model/setting.js +++ b/app/model/setting.js @@ -61,6 +61,10 @@ module.exports = app => { github: { clientID: { type: String, default: '' }, clientSecret: { type: String, default: '' } + }, + // 百度seo token + baiduSeo: { + token: { type: String, default: '' } } }, limit: { diff --git a/app/service/seo.js b/app/service/seo.js new file mode 100644 index 0000000..89f6dee --- /dev/null +++ b/app/service/seo.js @@ -0,0 +1,57 @@ +/** + * @desc SEO 相关 + */ + +const axios = require('axios') +const { Service } = require('egg') + +module.exports = class SeoService extends Service { + get baiduSeoClient () { + return axios.create({ + baseURL: 'http://data.zz.baidu.com', + headers: { + 'Content-Type': 'text/plain' + }, + params: { + site: this.config.author.url, + token: this.baiduSeoToken + } + }) + } + + get baiduSeoToken () { + try { + return this.app.setting.keys.baiduSeo.token + } catch (e) { + return '' + } + } + + // 百度seo push + async baiduSeo (type = '', urls = []) { + if (!this.baiduSeoToken) { + return this.logger.warn('未找到百度SEO token') + } + const actionMap = { + push: { url: '/urls', title: '推送' }, + update: { url: '/update', title: '更新' }, + delete: { url: '/del', title: '删除' } + } + const action = actionMap[type] + if (!action) return + const res = await axios.post( + `http://data.zz.baidu.com${action.url}?site=${this.config.author.url}&token=${this.baiduSeoToken}`, + urls, + { + headers: { + 'Content-Type': 'text/plain' + } + } + ) + if (res && res.status === 200) { + this.logger.info(`百度SEO${action.title}成功:${JSON.stringify(res.data)}`) + } else { + this.logger.error(`百度SEO${action.title}失败:${res.data && res.data.message}`) + } + } +} From aae92df2d4c7cfe1fdd1680431e3243c0a5bbc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Fri, 12 Oct 2018 23:25:33 +0800 Subject: [PATCH 203/208] fix: fix article archive - the order on month --- app/service/article.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/service/article.js b/app/service/article.js index cbb4a37..91a7aed 100644 --- a/app/service/article.js +++ b/app/service/article.js @@ -62,7 +62,7 @@ module.exports = class ArticleService extends ProxyService { } let data = await this.aggregate([ { $match }, - { $sort: { createdAt: 1 } }, + { $sort: { createdAt: -1 } }, { $project }, { $group: { @@ -84,7 +84,8 @@ module.exports = class ArticleService extends ProxyService { ]) let total = 0 if (data && data.length) { - data = [...new Set(data.map(item => item._id.year))].map(year => { + // 先取出year,并且降序排列,再填充month + data = [...new Set(data.map(item => item._id.year).sort((a, b) => b - a))].map(year => { const months = [] data.forEach(item => { const { _id, articles } = item From bbd24ea2badca08855c2e2f7acfe2a8a4f5a9cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sat, 13 Oct 2018 00:04:17 +0800 Subject: [PATCH 204/208] fix: fix github fetch user error --- app/service/github.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/service/github.js b/app/service/github.js index 512b594..7b34b52 100644 --- a/app/service/github.js +++ b/app/service/github.js @@ -42,7 +42,7 @@ module.exports = class GithubService extends Service { // 测试环境下 用测试配置 gayhub = this.config.github } else { - const { keys } = this.service.setting.getItem() + const { keys } = this.app.setting if (!keys || !keys.github) { this.logger.warn('未找到GitHub配置') return null From 2a166618496e431da99c22cfe507f91f71628b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sat, 13 Oct 2018 00:38:39 +0800 Subject: [PATCH 205/208] fix: add marked sanitize control --- .release-it.json | 2 +- CHANGELOG.md | 9 +++++++++ app/controller/comment.js | 4 ++-- app/model/comment.js | 4 ++-- app/schedule/personal.js | 3 +-- app/utils/markdown.js | 7 +++++-- package.json | 4 ++-- 7 files changed, 22 insertions(+), 11 deletions(-) diff --git a/.release-it.json b/.release-it.json index 83acbb1..816a155 100644 --- a/.release-it.json +++ b/.release-it.json @@ -14,7 +14,7 @@ "requireUpstream": true, "src": { "commit": true, - "commitMessage": "[chore] release %s", + "commitMessage": "chore: release %s", "commitArgs": "", "tag": true, "tagName": "release-v%s", diff --git a/CHANGELOG.md b/CHANGELOG.md index 4333667..d1ac16d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # CHANGELOG +## v2.0.2 + +2018-10-12 + +* fix: github获取用户信息时clientID和clientSecret错误 +* fix: add marked sanitize control +* fix: archive接口的月维度数据排序错误 +* fix: 关联文章排序错误 + ## v2.0.1 2018-10-09 diff --git a/app/controller/comment.js b/app/controller/comment.js index 322480c..b2c85ad 100644 --- a/app/controller/comment.js +++ b/app/controller/comment.js @@ -218,7 +218,7 @@ module.exports = class CommentController extends Controller { return ctx.fail('检测为垃圾评论,请修改后在提交') } this.logger.info('评论检测正常,可以发布') - body.renderedContent = this.app.utils.markdown.render(body.content) + body.renderedContent = this.app.utils.markdown.render(body.content, true) const comment = await this.service.comment.create(body) if (comment) { const data = await this.service.comment.getItemById(comment._id) @@ -294,7 +294,7 @@ module.exports = class CommentController extends Controller { } } if (body.content) { - body.renderedContent = this.app.utils.markdown.render(body.content) + body.renderedContent = this.app.utils.markdown.render(body.content, true) } let data = null if (!ctx.session._isAuthed) { diff --git a/app/model/comment.js b/app/model/comment.js index dc0641d..381c7a5 100644 --- a/app/model/comment.js +++ b/app/model/comment.js @@ -54,7 +54,7 @@ module.exports = app => { pre: { save (next) { if (this.content) { - this.renderedContent = app.utils.markdown.render(this.content) + this.renderedContent = app.utils.markdown.render(this.content, true) } next() }, @@ -64,7 +64,7 @@ module.exports = app => { const find = await this.model.findOne(this._conditions) if (find) { if (content && content !== find.content) { - this._update.renderedContent = app.utils.markdown.render(content) + this._update.renderedContent = app.utils.markdown.render(content, true) this._update.updatedAt = Date.now() } } diff --git a/app/schedule/personal.js b/app/schedule/personal.js index c9fb00b..2ee596e 100644 --- a/app/schedule/personal.js +++ b/app/schedule/personal.js @@ -9,8 +9,7 @@ module.exports = class UpdatePersonalGithubInfo extends Subscription { return { // 每小时更新一次 interval: '1h', - type: 'worker', - immediate: true + type: 'worker' } } diff --git a/app/utils/markdown.js b/app/utils/markdown.js index 95802ce..0be764c 100644 --- a/app/utils/markdown.js +++ b/app/utils/markdown.js @@ -102,7 +102,7 @@ marked.setOptions({ renderer, gfm: true, pedantic: false, - sanitize: true, + sanitize: false, tables: true, breaks: true, headerIds: true, @@ -125,4 +125,7 @@ function escape (html, encode) { .replace(/'/g, ''') } -exports.render = marked +exports.render = (text, sanitize = false) => { + marked.setOptions({ sanitize }) + return marked(text) +} diff --git a/package.json b/package.json index 967e261..09a3acf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "2.0.1", + "version": "2.0.2", "description": "", "private": true, "dependencies": { @@ -84,7 +84,7 @@ "pre-git": { "commit-msg": "simple", "pre-commit": [ - "yarn lint" + "yarn lint" ], "pre-push": [], "post-commit": [], From 0c7c2b5eccc025140a43a566fd0e68b9daa7f15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sat, 13 Oct 2018 00:40:16 +0800 Subject: [PATCH 206/208] chore: release 2.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09a3acf..2dad676 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-server", - "version": "2.0.2", + "version": "2.0.3", "description": "", "private": true, "dependencies": { From 685974ca57197629446b9c8cead2879b327d90a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sat, 13 Oct 2018 02:49:35 +0800 Subject: [PATCH 207/208] feat: marked render image add title --- app/controller/article.js | 6 +++--- app/utils/markdown.js | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/controller/article.js b/app/controller/article.js index 2fbe574..d094779 100644 --- a/app/controller/article.js +++ b/app/controller/article.js @@ -188,7 +188,7 @@ module.exports = class ArticleController extends Controller { const data = await this.service.article.create(body) if (data) { ctx.success(data, '文章创建成功') - if (!this.config.isProd) { + if (this.config.isProd) { this.service.seo.baiduSeo('push', `${this.config.author.url}/article/${data._id}`) } } else { @@ -225,7 +225,7 @@ module.exports = class ArticleController extends Controller { ) if (data) { ctx.success(data, '文章更新成功') - if (!this.config.isProd) { + if (this.config.isProd) { this.service.seo.baiduSeo('update', `${this.config.author.url}/article/${data._id}`) } } else { @@ -239,7 +239,7 @@ module.exports = class ArticleController extends Controller { const data = await this.service.article.deleteItemById(params.id) if (data) { ctx.success(data, '文章删除成功') - if (!this.config.isProd) { + if (this.config.isProd) { this.service.seo.baiduSeo('delete', `${this.config.author.url}/article/${data._id}`) } } else { diff --git a/app/utils/markdown.js b/app/utils/markdown.js index 0be764c..809e095 100644 --- a/app/utils/markdown.js +++ b/app/utils/markdown.js @@ -42,12 +42,14 @@ renderer.link = function (href, title, text) { } renderer.image = function (href, title, text) { + const _title = title || text || '' return ` ${text || title || href} + ${this.options.xhtml ? '/' : ''}> + ${_title ? `

《${_title}》

` : ''} `.replace(/\s+/g, ' ').replace('\n', '') } From 75b92c25aa4dd532d10a72567aa5ffe2d75ea7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E5=BF=97=E6=B4=8B?= Date: Sat, 13 Oct 2018 03:04:57 +0800 Subject: [PATCH 208/208] fix: fix marked render image --- app/utils/markdown.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/utils/markdown.js b/app/utils/markdown.js index 809e095..3c9b605 100644 --- a/app/utils/markdown.js +++ b/app/utils/markdown.js @@ -44,12 +44,14 @@ renderer.link = function (href, title, text) { renderer.image = function (href, title, text) { const _title = title || text || '' return ` - ${text || title || href} - ${_title ? `

《${_title}》

` : ''} +

+ ${text || title || href} + ${_title ? `

《${_title}》

` : ''} +

`.replace(/\s+/g, ' ').replace('\n', '') }