├── .gitignore ├── .gitmodules ├── .prettierignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── lerna.json ├── package.json ├── server ├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── bin │ ├── debug.js │ ├── development.js │ └── production.js ├── package.json ├── src │ ├── app.js │ ├── config │ │ ├── index.js │ │ ├── main.js │ │ └── passport.js │ ├── controllers │ │ ├── authCtrl.js │ │ ├── helpCtrl.js │ │ └── linksCtrl.js │ ├── helper │ │ ├── errorMsgConf.js │ │ ├── logger.js │ │ ├── nodemailer.js │ │ ├── successMsgConf.js │ │ └── util.js │ ├── index.js │ ├── middlewares │ │ ├── cache.js │ │ └── index.js │ ├── models │ │ ├── index.js │ │ ├── linksModel.js │ │ └── userModel.js │ ├── routes │ │ └── index.js │ └── services │ │ ├── apiCache.js │ │ ├── pageCache.js │ │ └── redisCache.js ├── test │ └── test.js ├── views │ ├── 404.ejs │ ├── 422.ejs │ ├── 500.ejs │ ├── docker-vue-node-nginx-mongodb-redis-dragon.png │ ├── error.ejs │ ├── index.ejs │ └── mailTemp.html └── yarn.lock └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # package-lock.json 5 | package-lock.json 6 | 7 | # server 8 | server/public 9 | 10 | # local env files 11 | .env.local 12 | .env.*.local 13 | 14 | # Log files 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "client"] 2 | path = client 3 | url = git@github.com:nicejade/awesome-vue-cli3-example.git 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # client 4 | client/dist 5 | 6 | # server 7 | server/app 8 | server/public 9 | server/logs 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 17koa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Docker Vue Node Nginx Mongodb Redis

4 | 5 |
6 | 🐉 An awesome boilerplate, Integrated Docker, Vue, Node, Nginx, Mongodb and Redis in one,
Designed to develop & build your web applications more efficient and elegant. 7 |
8 | 9 |
10 | 11 |
12 | 13 | NodeJs 14 | 15 | 16 | LICENSE 17 | 18 | 19 | LICENSE 20 | 21 | Author nicejade 22 |
23 | 24 | ## Features 25 | 26 | - Powered by [Vue2.\*](https://vuejs.org/) & [Koa2.\*](https://koajs.com/) & [Mongodb](https://www.mongodb.com/) & [Nginx](https://www.nginx.com/) & [Redis](https://redis.io/) & [Docker](https://docs.docker.com/compose/install/) 27 | - Integrate Front-End, Back-End, Database into `Docker` 28 | - Rich features and constantly optimized design 29 | - Based on the awesome third-party libraries 30 | 31 | ## Prerequisites 32 | 33 | [Node.js](https://nodejs.org/en/) (>=4.x, 8.x preferred), Npm version 4+([Yarn](https://jeffjade.com/2017/12/30/135-npm-vs-yarn-detial-memo/) preferred), [Git](https://git-scm.com/), [Mongodb](https://www.mongodb.com/), [Nginx](https://www.nginx.com/), [Redis](https://redis.io/) and [Docker](https://docs.docker.com/compose/install/). 34 | 35 | ## Getting started 36 | 37 | ```bash 38 | # 🎉 clone the project 39 | git clone https://github.com/nicejade/docker-vue-node-nginx-mongodb-redis.git 40 | # ➕ install dependencies 41 | cd docker-vue-node-nginx-mongodb-redis 42 | yarn && yarn bootstrap 43 | 44 | # 🚧 start developing 45 | yarn start 46 | 47 | # Or Run the following commands in the terminal two different TAB 48 | cd client && yarn start 49 | cd server && yarn start 50 | ``` 51 | 52 | The program will automatically open http://localhost:8080/ for client and http://localhost:4000/ for server. Intelligently, it will specify the available port for you (incremental, eg: `8081` or `8082`) if port `8080` is busying on your machine. 53 | 54 | ## Deployment 55 | 56 | ```bash 57 | # 🚀 deploy your client & server(local or server) 58 | yarn deploy 59 | 60 | # Or Run the following command at root directory 61 | docker-compose up 62 | ``` 63 | 64 | ## Links 65 | 66 | - [**NICE LINKS**](https://nicelinks.site?from=github) 67 | - [About Me](https://about.me/nicejade/) 68 | - [Latest blog](https://nice.lovejade.cn/) 69 | - [First Blog](https://jeffjade.com/) 70 | - [Second Blog](https://blog.lovejade.cn/) 71 | - [Weibo](https://weibo.com/jeffjade) 72 | - [ZhiHu](https://www.zhihu.com/people/yang-qiong-pu/) 73 | - [SegmentFault](https://segmentfault.com/u/jeffjade) 74 | - [JianShu](http://www.jianshu.com/u/9aae3d8f4c3d) 75 | - [Twitter](https://twitter.com/nicejadeyang) 76 | - [Facebook](https://www.facebook.com/yang.gang.jade) 77 | 78 | | 微信公众号 | 前端微信群 | 推荐 Web 应用 | 79 | | --- | --- | --- | 80 | | 😉 静晴轩 | ✨ 大前端联盟 | 🎉 倾城之链 | 81 | | ![静晴轩](https://image.nicelinks.site/qrcode_jqx.jpg) | ![倾城之链](https://image.nicelinks.site/wqycx-weixin.png?ver=1) |倾城之链| 82 | 83 | ## License 84 | 85 | [MIT](http://opensource.org/licenses/MIT) 86 | 87 | Copyright (c) 2018-present, [nicejade](https://aboutme.lovejade.cn/?utm_source=github.com&pid=docker-vue-node-nginx-mongodb-redis). 88 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Node, Mongo, Redis, Nginx 2 | 3 | version: "2" 4 | services: 5 | server: 6 | image: node:8.12.0 7 | restart: on-failure 8 | ports: 9 | - "4000:4000" 10 | links: 11 | - mongodb 12 | - redis 13 | depends_on: 14 | - mongodb 15 | - redis 16 | volumes: 17 | - ./server:/home/node/awesome-webapp/ 18 | working_dir: /home/node/awesome-webapp 19 | command: yarn server 20 | networks: 21 | - backend 22 | logging: 23 | driver: "json-file" 24 | options: 25 | max-size: "100MB" 26 | max-file: "3" 27 | 28 | nginx: 29 | image: nginx:1.15.3 30 | depends_on: 31 | - server 32 | networks: 33 | - backend 34 | volumes: 35 | - ./server/public:/home/node/awesome-webapp/public/ 36 | - ./nginx/default.conf:/etc/nginx/conf.d/awesome-webapp.conf 37 | ports: 38 | - "8888:8888" 39 | logging: 40 | driver: "json-file" 41 | options: 42 | max-size: "100MB" 43 | max-file: "3" 44 | 45 | mongodb: 46 | image: mongo:4.1.3 47 | ports: 48 | - "27017:27017" 49 | volumes: 50 | - mongodb:/data/db/ 51 | networks: 52 | - backend 53 | logging: 54 | driver: "json-file" 55 | options: 56 | max-size: "100MB" 57 | max-file: "3" 58 | 59 | redis: 60 | image: redis:4.0.11 61 | ports: 62 | - "6379:6379" 63 | networks: 64 | - backend 65 | volumes: 66 | - redis:/data/ 67 | logging: 68 | driver: "json-file" 69 | options: 70 | max-size: "100MB" 71 | max-file: "3" 72 | 73 | networks: 74 | backend: 75 | 76 | volumes: 77 | mongodb: 78 | redis: 79 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "3.0.3", 3 | "packages": [ 4 | "client", 5 | "server" 6 | ], 7 | "ignoreChanges": [ 8 | "**/__tests__/**", 9 | "**/*.md" 10 | ], 11 | "version": "0.0.0" 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-vue-node-nginx-mongodb-redis", 3 | "version": "0.1.0", 4 | "private": true, 5 | "author": "nicejade", 6 | "scripts": { 7 | "bootstrap": "lerna bootstrap --npm-client=yarn", 8 | "start": "lerna exec --scope -- yarn start", 9 | "commit": "git add . && git commit -a && git push", 10 | "deploy": "docker-compose down && docker-compose up", 11 | "prettier": "prettier --write \"{client,server}/**/*.{js,css,scss,vue}\"", 12 | "watcher": "onchange '**/*.md' \"{client,server}/**/**/*.{js,css,scss,vue}\" -- prettier --write {{changed}}", 13 | "eslint-fix": "eslint src/**/**/*.vue --fix", 14 | "format-code": "npm run format-client & npm run format-server", 15 | "format-client": "prettier-eslint --write \"client/src/**/*.js\" \"client/src/**/*.vue\"", 16 | "format-server": "prettier-eslint --write \"server/src/**/*.js\"", 17 | "client:start": "lerna exec --scope client -- yarn start", 18 | "client:build": "lerna exec --scope client -- yarn build", 19 | "server:start": "lerna exec --scope server -- yarn start", 20 | "server:build": "lerna exec --scope client -- yarn build" 21 | }, 22 | "keywords": [ 23 | "docker", 24 | "vue", 25 | "node", 26 | "nginx", 27 | "mongodb", 28 | "redis" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/nicejade/docker-vue-node-nginx-mongodb-redis.git" 33 | }, 34 | "homepage": "https://nicelinks.site", 35 | "license": "MIT", 36 | "dependencies": {}, 37 | "devDependencies": { 38 | "eslint-config-prettier": "^3.0.1", 39 | "eslint-plugin-prettier": "^2.6.2", 40 | "husky": "^4.3.0", 41 | "lerna": "3.22.1", 42 | "lint-staged": "^7.2.2", 43 | "prettier-eslint-cli": "^4.7.1" 44 | }, 45 | "eslintConfig": { 46 | "root": true, 47 | "env": { 48 | "node": true, 49 | "es6": true 50 | }, 51 | "rules": { 52 | "no-console": 0, 53 | "no-useless-escape": 0, 54 | "no-multiple-empty-lines": [ 55 | 2, 56 | { 57 | "max": 3 58 | } 59 | ], 60 | "prettier/prettier": [ 61 | "error", 62 | { 63 | "singleQuote": true, 64 | "semi": false, 65 | "trailingComma": "none", 66 | "bracketSpacing": true, 67 | "jsxBracketSameLine": true, 68 | "insertPragma": true, 69 | "requirePragma": false 70 | } 71 | ] 72 | }, 73 | "plugins": [], 74 | "extends": [ 75 | "plugin:vue/essential", 76 | "plugin:prettier/recommended", 77 | "eslint:recommended" 78 | ], 79 | "parserOptions": { 80 | "parser": "babel-eslint" 81 | } 82 | }, 83 | "prettier": { 84 | "singleQuote": true, 85 | "semi": false, 86 | "printWidth": 100, 87 | "proseWrap": "never" 88 | }, 89 | "husky": { 90 | "hooks": { 91 | "pre-commit": "lint-staged" 92 | } 93 | }, 94 | "lint-staged": { 95 | "**/**.{js,json,pcss,md,vue}": [ 96 | "prettier --write", 97 | "git add" 98 | ] 99 | }, 100 | "eslintIgnore": [ 101 | "package.json" 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-loose", "stage-3"], 3 | "plugins": ["add-module-exports", "transform-runtime"], 4 | "sourceMaps": true, 5 | "retainLines": true 6 | } 7 | -------------------------------------------------------------------------------- /server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "standard", 4 | "env": { 5 | "node": true, 6 | "es6": true, 7 | "mocha": true 8 | }, 9 | "ecmaFeatures": { 10 | "arrowFunctions": true 11 | }, 12 | "rules": { 13 | "func-style": [2, "declaration", { "allowArrowFunctions": true }] 14 | } 15 | } -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # app // compiled js 36 | app 37 | 38 | # VSCODE 39 | .vscode 40 | 41 | # JETBRAIN 42 | .idea 43 | 44 | !src/config/index.js 45 | !src/config/default.js.idea 46 | 47 | src/config/secret.js 48 | 49 | upload -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | ## Koa2 Server 2 | 3 | ## Prerequisites 4 | 5 | [Node.js](https://nodejs.org/en/) (>= 8.*), Npm version 4+(Yarn preferred), and [Git](https://git-scm.com/). 6 | 7 | ## Usage 8 | 9 | ```bash 10 | cd server 11 | yarn && yarn start 12 | ``` 13 | 14 | ## Recommended links 15 | 16 | - [**NICE LINKS**](https://nicelinks.site?from=github) 17 | - [About Me](https://about.me/nicejade/) 18 | - [First Blog](https://jeffjade.com) 19 | - [Second Blog](https://blog.lovejade.cn/) 20 | - [Weibo](http://weibo.com/jeffjade) 21 | - [ZhiHu](https://www.zhihu.com/people/yang-qiong-pu/) 22 | - [SegmentFault](https://segmentfault.com/u/jeffjade) 23 | - [JianShu](http://www.jianshu.com/u/9aae3d8f4c3d) 24 | - [Twitter](https://twitter.com/jeffjade2) 25 | - [Facebook](https://www.facebook.com/yang.gang.jade) 26 | 27 | ## License 28 | 29 | [MIT](http://opensource.org/licenses/MIT) 30 | 31 | Copyright (c) 2018-present, [nicejade](https://about.me/nicejade/). -------------------------------------------------------------------------------- /server/bin/debug.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // debug 还是实验性功能 3 | var path = require('path') 4 | var babelCliDir = require('babel-cli/lib/babel/dir') 5 | require('colors') 6 | console.log('>>> [DEBUG]: Debug Mode is an expiremental feature'.cyan) 7 | console.log('>>> [DEBUG]: Compiling...'.green) 8 | babelCliDir({ outDir: 'app/', retainLines: true, sourceMaps: true }, ['src/']) // compile all when start 9 | 10 | try { 11 | require(path.join(__dirname, '../app')) 12 | } catch (e) { 13 | if (e && e.code === 'MODULE_NOT_FOUND') { 14 | console.log('>>> [DEBUG]: run `npm compile` first!') 15 | process.exit(1) 16 | } 17 | console.log('>>> [DEBUG]: App started with error and exited'.red, e) 18 | process.exit(1) 19 | } 20 | 21 | console.log('>>> [DEBUG]: App started in debug mode'.green) 22 | -------------------------------------------------------------------------------- /server/bin/development.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var path = require('path') 3 | var projectRootPath = path.resolve(__dirname, '..') 4 | var srcPath = path.join(projectRootPath, 'src') 5 | var appPath = path.join(projectRootPath, 'app') 6 | var fs = require('fs') 7 | var debug = require('debug')('dev') 8 | require('colors') 9 | var log = console.log.bind(console, '>>> [DEV]:'.red) 10 | var babelCliDir = require('babel-cli/lib/babel/dir') 11 | var babelCliFile = require('babel-cli/lib/babel/file') 12 | var chokidar = require('chokidar') 13 | var watcher = chokidar.watch(path.join(__dirname, '../src')) 14 | 15 | watcher.on('ready', function() { 16 | log('Compiling...'.green) 17 | babelCliDir({ outDir: 'app/', retainLines: true, sourceMaps: true }, ['src/']) // compile all when start 18 | require('../app') // start app 19 | log('♪ App Started'.green) 20 | 21 | watcher 22 | .on('add', function(absPath) { 23 | compileFile('src/', 'app/', path.relative(srcPath, absPath), cacheClean) 24 | }) 25 | .on('change', function(absPath) { 26 | compileFile('src/', 'app/', path.relative(srcPath, absPath), cacheClean) 27 | }) 28 | .on('unlink', function(absPath) { 29 | var rmfileRelative = path.relative(srcPath, absPath) 30 | var rmfile = path.join(appPath, rmfileRelative) 31 | try { 32 | fs.unlinkSync(rmfile) 33 | fs.unlinkSync(rmfile + '.map') 34 | } catch (e) { 35 | debug('fail to unlink', rmfile) 36 | return 37 | } 38 | console.log('Deleted', rmfileRelative) 39 | cacheClean() 40 | }) 41 | }) 42 | 43 | function compileFile(srcDir, outDir, filename, cb) { 44 | var outFile = path.join(outDir, filename) 45 | var srcFile = path.join(srcDir, filename) 46 | try { 47 | babelCliFile( 48 | { 49 | outFile: outFile, 50 | retainLines: true, 51 | highlightCode: true, 52 | comments: true, 53 | babelrc: true, 54 | sourceMaps: true 55 | }, 56 | [srcFile], 57 | { highlightCode: true, comments: true, babelrc: true, ignore: [], sourceMaps: true } 58 | ) 59 | } catch (e) { 60 | console.error('Error while compiling file %s', filename, e) 61 | return 62 | } 63 | console.log(srcFile + ' -> ' + outFile) 64 | cb && cb() 65 | } 66 | 67 | function cacheClean() { 68 | Object.keys(require.cache).forEach(function(id) { 69 | if (/[\/\\](app)[\/\\]/.test(id)) { 70 | delete require.cache[id] 71 | } 72 | }) 73 | log('♬ App Cache Cleaned...'.green) 74 | } 75 | 76 | process.on('exit', function(e) { 77 | log(' ♫ App Quit'.green) 78 | }) 79 | -------------------------------------------------------------------------------- /server/bin/production.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var path = require('path') 3 | 4 | try { 5 | require(path.join(__dirname, '../app')) 6 | } catch (e) { 7 | if (e && e.code === 'MODULE_NOT_FOUND') { 8 | console.log('run `npm compile` first!') 9 | process.exit(1) 10 | } 11 | console.log('app started with error and exited', e) 12 | process.exit(1) 13 | } 14 | 15 | console.log('app started in production mode') 16 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.1", 4 | "author": "nicejade", 5 | "scripts": { 6 | "start": "npm run dev", 7 | "dev": "cross-env NODE_ENV=development nodemon src/index.js", 8 | "production": "node bin/production.js", 9 | "debug": "npm run clean && node bin/debug", 10 | "clean": "rm -rf app/", 11 | "build": "npm test && npm run clean && npm run compile", 12 | "compile": "babel src/ --out-dir app/ --retain-lines --source-maps", 13 | "test": "mocha -u bdd --compilers js:babel-core/register", 14 | "server": "npm run clean && npm run compile && npm run production", 15 | "deploy": "npm run clean && npm run compile && cross-env NODE_ENV=production pm2 restart bin/production.js --name 'awesome-webapp'" 16 | }, 17 | "engines": { 18 | "node": ">= 4" 19 | }, 20 | "dependencies": { 21 | "axios": "^0.21.1", 22 | "babel-cli": "^6.7.7", 23 | "babel-core": "^6.26.3", 24 | "babel-plugin-add-module-exports": "^1.0.2", 25 | "babel-plugin-transform-runtime": "^6.8.0", 26 | "babel-preset-es2015": "^6.6.0", 27 | "babel-preset-es2015-loose": "^8.0.0", 28 | "babel-preset-stage-3": "^6.5.0", 29 | "babel-runtime": "^6.6.1", 30 | "bcrypt-nodejs": "0.0.3", 31 | "bluebird": "^3.7.1", 32 | "cheerio": "^1.0.0-rc.3", 33 | "cross-env": "^6.0.3", 34 | "crypto": "1.0.1", 35 | "debug": "^4.1.1", 36 | "ejs": "^2.7.4", 37 | "formidable": "^1.1.1", 38 | "fs-extra": "^8.1.0", 39 | "jsonwebtoken": "^8.5.1", 40 | "kcors": "^2.2.2", 41 | "koa": "^2.11.0", 42 | "koa-bodyparser": "^4.2.1", 43 | "koa-convert": "^1.2.0", 44 | "koa-helmet": "^5.2.0", 45 | "koa-json": "^2.0.2", 46 | "koa-logger": "^3.2.1", 47 | "koa-mount": "^4.0.0", 48 | "koa-onerror": "^4.1.0", 49 | "koa-passport": "4.1.3", 50 | "koa-redis": "^4.0.0", 51 | "koa-router": "^7.4.0", 52 | "koa-session": "^5.12.3", 53 | "koa-session2": "^2.2.10", 54 | "koa-static": "^5.0.0", 55 | "koa-views": "^6.2.1", 56 | "koa2-cors": "^2.0.6", 57 | "lodash": "^4.17.15", 58 | "mongo-sanitize": "^1.0.1", 59 | "mongoose": "^5.7.12", 60 | "nodemailer": "^6.3.1", 61 | "passport": "^0.4.0", 62 | "passport-jwt": "2.2.1", 63 | "passport-local": "^1.0.0", 64 | "redis": "^2.8.0", 65 | "sha1": "^1.1.1", 66 | "winston": "2.3.1" 67 | }, 68 | "devDependencies": { 69 | "babel-eslint": "^10.0.3", 70 | "chokidar": "^3.3.0", 71 | "colors": "^1.4.0", 72 | "eslint": "^6.6.0", 73 | "eslint-config-standard": "^14.1.0", 74 | "eslint-plugin-promise": "^4.2.1", 75 | "eslint-plugin-standard": "^4.0.1", 76 | "mocha": "^6.2.2", 77 | "should": "^13.2.3", 78 | "supertest": "^4.0.2" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server/src/app.js: -------------------------------------------------------------------------------- 1 | let http = require('http') 2 | let Koa = require('koa') 3 | let koaOnError = require('koa-onerror') 4 | let logger = require('./helper/logger') 5 | 6 | let applyMiddleware = require('./middlewares').applyMiddleware 7 | let config = require('./config') 8 | 9 | const app = new Koa() 10 | applyMiddleware(app) 11 | 12 | // 500 error 13 | koaOnError(app, { 14 | template: 'views/500.ejs' 15 | }) 16 | 17 | // error logger 18 | app.on('error', async (err, ctx) => { 19 | logger.error('app.on error:', { err: err.stack }) 20 | }) 21 | 22 | const port = parseInt(config.main.port || '3000') 23 | const server = http.createServer(app.callback()) 24 | 25 | server.listen(port) 26 | server.on('error', error => { 27 | if (error.syscall !== 'listen') { 28 | throw error 29 | } 30 | // handle specific listen errors with friendly messages 31 | switch (error.code) { 32 | case 'EACCES': 33 | console.error(port + ' requires elevated privileges') 34 | process.exit(1) 35 | break 36 | case 'EADDRINUSE': 37 | console.error(port + ' is already in use') 38 | process.exit(1) 39 | break 40 | default: 41 | throw error 42 | } 43 | }) 44 | 45 | server.on('listening', () => { 46 | console.log('Listening on port: %d', port) 47 | }) 48 | 49 | module.exports = app 50 | -------------------------------------------------------------------------------- /server/src/config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | main: require('./main.js'), 3 | passport: require('./passport.js'), 4 | isOpenRedisFlag: true 5 | } 6 | -------------------------------------------------------------------------------- /server/src/config/main.js: -------------------------------------------------------------------------------- 1 | let { join } = require('path') 2 | const isDevelopmentEnv = process.env.NODE_ENV && process.env.NODE_ENV === 'development' 3 | const dbhost = isDevelopmentEnv ? 'localhost' : 'mongodb' 4 | const redishost = isDevelopmentEnv ? '127.0.0.1' : 'redis' 5 | 6 | let config = { 7 | env: isDevelopmentEnv ? 'development' : 'production', 8 | 9 | // Secret key for JWT signing and encryption 10 | secret: 'super-secret-passphrase', 11 | 12 | // Database connection information 13 | database: `mongodb://${dbhost}:27017/awesome-webapp`, 14 | 15 | // Setting port for server 16 | port: process.env.PORT || 4000, 17 | 18 | redis: { 19 | session: { 20 | host: `${redishost}`, 21 | port: 6379, 22 | db: 0 23 | }, 24 | client: { 25 | host: `${redishost}`, 26 | port: 6379, 27 | db: 1 28 | } 29 | }, 30 | 31 | // Avatar upload path 32 | avatarUploadDir: join(__dirname, './../../upload/avatar/') 33 | } 34 | 35 | module.exports = config 36 | -------------------------------------------------------------------------------- /server/src/config/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require('koa-passport'), 2 | User = require('../models/userModel'), 3 | config = require('./main'), 4 | JwtStrategy = require('passport-jwt').Strategy, 5 | ExtractJwt = require('passport-jwt').ExtractJwt, 6 | LocalStrategy = require('passport-local') 7 | 8 | const localOptions = { usernameField: 'email' } 9 | // Setting up local login strategy 10 | const localEmailLogin = new LocalStrategy(localOptions, function(email, password, done) { 11 | User.findOne({ email: email }, function(err, user) { 12 | if (err) { 13 | return done(err) 14 | } 15 | if (!user) { 16 | return done(null, false, { 17 | error: 'Your login details could not be verified. Please try again.' 18 | }) 19 | } 20 | 21 | user.comparePassword(password, function(err, isMatch) { 22 | if (err) { 23 | return done(err) 24 | } 25 | if (!isMatch) { 26 | return done(null, false, { 27 | error: 'Your login details could not be verified. Please try again.' 28 | }) 29 | } 30 | 31 | return done(null, user) 32 | }) 33 | }) 34 | }) 35 | 36 | const localUsernameLogin = new LocalStrategy({ usernameField: 'username' }, function( 37 | username, 38 | password, 39 | done 40 | ) { 41 | User.findOne({ username: username }, function(err, user) { 42 | if (err) { 43 | return done(err) 44 | } 45 | if (!user) { 46 | return done(null, false, { 47 | error: 'Your login details could not be verified. Please try again.' 48 | }) 49 | } 50 | 51 | user.comparePassword(password, function(err, isMatch) { 52 | if (err) { 53 | return done(err) 54 | } 55 | if (!isMatch) { 56 | return done(null, false, { 57 | error: 'Your login details could not be verified. Please try again.' 58 | }) 59 | } 60 | 61 | return done(null, user) 62 | }) 63 | }) 64 | }) 65 | 66 | const jwtOptions = { 67 | // Telling Passport to check authorization headers for JWT 68 | jwtFromRequest: ExtractJwt.fromAuthHeader(), 69 | // Telling Passport where to find the secret 70 | secretOrKey: config.secret 71 | } 72 | 73 | // Setting up JWT login strategy 74 | const jwtLogin = new JwtStrategy(jwtOptions, function(payload, done) { 75 | User.findById(payload._id, function(err, user) { 76 | if (err) { 77 | return done(err, false) 78 | } 79 | 80 | if (user) { 81 | done(null, user) 82 | } else { 83 | done(null, false) 84 | } 85 | }) 86 | }) 87 | 88 | // serializeUser 在用户登录验证成功以后将会把用户的数据存储到 session 中 89 | passport.serializeUser(function(user, done) { 90 | done(null, user) 91 | }) 92 | 93 | // deserializeUser 在每次请求的时候将从 session 中读取用户对象 94 | passport.deserializeUser(function(user, done) { 95 | return done(null, user) 96 | }) 97 | 98 | passport.use(jwtLogin) 99 | passport.use('email-local', localEmailLogin) 100 | passport.use('username-local', localUsernameLogin) 101 | 102 | module.exports = passport 103 | -------------------------------------------------------------------------------- /server/src/controllers/authCtrl.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'), 2 | crypto = require('crypto'), 3 | UserModel = require('../models/userModel'), 4 | config = require('../config/main'), 5 | passport = require('../config/passport'), 6 | sendMail = require('../helper/nodemailer'), 7 | $util = require('../helper/util') 8 | 9 | // Middleware to require login/auth 10 | const requireAuth = passport.authenticate('jwt', { session: false }) 11 | 12 | function generateToken(info) { 13 | return jwt.sign(info, config.secret, { 14 | expiresIn: 80 // in seconds 15 | }) 16 | } 17 | 18 | const logoffUserById = id => { 19 | return new Promise((resolve, reject) => { 20 | UserModel.findOneAndRemove({ _id: id }, err => { 21 | if (err) { 22 | reject(err) 23 | } 24 | resolve() 25 | }) 26 | }) 27 | } 28 | 29 | const setTokenAndSendMail = async (user, ctx) => { 30 | // 保证激活码不会重复 31 | let buf = crypto.randomBytes(20) 32 | user.activeToken = user._id + buf.toString('hex') 33 | user.activeExpires = Date.now() + 48 * 3600 * 1000 34 | let link = `${config.clientPath}/account?activeToken=` + user.activeToken 35 | 36 | // 发送激活邮件;如果参数中没有 Email,则取 username 查出; 37 | if (!user.email || !$util.verifyIsLegalEmail(user.email)) { 38 | let foundUser = await $util.findUser({ username: user.username }) 39 | user.email = foundUser.username === user.username && foundUser.email 40 | } 41 | sendMail({ to: user.email, type: 'active', link: link }) 42 | try { 43 | await new Promise((resolve, reject) => { 44 | user.save(err => { 45 | if (err) { 46 | reject(err) 47 | } 48 | resolve() 49 | }) 50 | }) 51 | } catch (err) { 52 | throw err 53 | } 54 | } 55 | 56 | const settingLoginResult = async (user, ctx) => { 57 | if (user) { 58 | if (!user.active) { 59 | // 如果用户没有及时激活,则在登录时候重新发送激活邮件; 60 | await setTokenAndSendMail(user, ctx) 61 | return $util.sendFailure(ctx, 'accountNoActive') 62 | } 63 | const timeOfValidity = 7 * 24 * 3600000 64 | const options = { 65 | maxAge: timeOfValidity, 66 | httpOnly: false 67 | } 68 | ctx.cookies.set('is-login', true, options) 69 | ctx.cookies.set('user-id', user._id, options) 70 | ctx.cookies.set('username', user.username, options) 71 | 72 | return $util.sendSuccess(ctx, { 73 | role: user.role, 74 | _id: user._id, 75 | username: user.username, 76 | profile: user.profile 77 | }) 78 | } else { 79 | return $util.sendFailure(ctx, 'wrongAccountOrPwd') 80 | } 81 | } 82 | 83 | // ======================================== 84 | // Login Route 85 | // ======================================== 86 | exports.checkIsExisted = async (ctx, next) => { 87 | const requestBody = ctx.request.body 88 | const username = requestBody.username 89 | const foundUser = await $util.findUser({ username: username }) 90 | if (foundUser) { 91 | return $util.sendFailure(ctx, 'nameHadRegistered') 92 | } else { 93 | return $util.sendSuccess(ctx, true) 94 | } 95 | } 96 | 97 | exports.login = (ctx, next) => { 98 | const requestBody = ctx.request.body 99 | const email = requestBody.email 100 | const username = requestBody.username 101 | 102 | /* 103 | Desc: 如果前端参数过来,有 email 且合法,则以 email 来作登录验证,否则用 username; 104 | Date: 2018-02-28 105 | */ 106 | if (email && $util.verifyIsLegalEmail(email)) { 107 | return passport.authenticate('email-local', (err, user, info, status) => { 108 | return settingLoginResult(user, ctx) 109 | })(ctx, next) 110 | } else { 111 | return passport.authenticate('username-local', (err, user, info, status) => { 112 | return settingLoginResult(user, ctx) 113 | })(ctx, next) 114 | } 115 | } 116 | 117 | exports.logout = (ctx, next) => { 118 | ctx.cookies.set('ns-is-login', false) 119 | ctx.status = 200 120 | $util.sendSuccess(ctx, 'logout successfully') 121 | } 122 | 123 | exports.logoff = async ctx => { 124 | let id = ctx.request.body.id 125 | await logoffUserById(id) 126 | ctx.status = 200 127 | ctx.body = { 128 | success: true, 129 | message: 'Logoff Success' 130 | } 131 | } 132 | 133 | // ======================================== 134 | // Registration Route 135 | // ======================================== 136 | exports.signup = async (ctx, next) => { 137 | const requestBody = ctx.request.body 138 | const email = requestBody.email 139 | const username = requestBody.username 140 | const password = requestBody.password 141 | 142 | // Return error if no email provided 143 | if (!email) { 144 | return $util.sendFailure(ctx, 'noUsername') 145 | } 146 | 147 | if (!username) { 148 | return $util.sendFailure(ctx, 'noUsername') 149 | } 150 | 151 | // Return error if no password provided 152 | if (!password) { 153 | return $util.sendFailure(ctx, 'noPassword') 154 | } 155 | 156 | const user = await $util.findUser({ email: email }) 157 | if (user) { 158 | // 如果已经注册但未激活,重新发送激活信息 159 | if (!user.active) { 160 | if (new Date(user.activeExpires) > new Date()) { 161 | return $util.sendFailure(ctx, 'pleaseActiveMailbox') 162 | } 163 | user.username = username 164 | user.password = password 165 | await setTokenAndSendMail(user, ctx) 166 | return $util.sendSuccessWithMsg(ctx, 'sendEmailSuccess', user.email) 167 | } else { 168 | return $util.sendFailure(ctx, 'mailboxHadRegistered') 169 | } 170 | } else { 171 | let user = new UserModel({ 172 | username: username, 173 | email: email, 174 | password: password, 175 | registeTime: new Date(), 176 | profile: {} 177 | }) 178 | await setTokenAndSendMail(user, ctx) 179 | return $util.sendSuccessWithMsg(ctx, 'sendEmailSuccess', user.email) 180 | } 181 | } 182 | 183 | // ======================================== 184 | // Active Account 185 | // ======================================== 186 | exports.active = async (ctx, next) => { 187 | const requestBody = ctx.request.body 188 | let user = await UserModel.findOne({ 189 | activeToken: requestBody.activeToken, 190 | // 过期时间 > 当前时间 191 | activeExpires: { $gt: Date.now() } 192 | }).exec() 193 | // 激活码无效 194 | if (!user) { 195 | return $util.sendFailure(ctx, 'activeValidationFailed') 196 | } 197 | // 激活并保存(同时设置用户的 number - 第几位注册用户) 198 | try { 199 | // let activatedNum = await UserModel.count({active: true}) 200 | // let activatedNum = await UserModel.find({active: true}).count() 201 | // const allUserList = await UserModel.find({active: true}) 202 | let userAggregateArr = await UserModel.aggregate([ 203 | { $match: { active: true } }, 204 | // a group specification must include an _id; 205 | { $group: { _id: null, count: { $sum: 1 } } } 206 | ]) 207 | user.number = userAggregateArr[0].count + 1 208 | user.active = true 209 | user.activeTime = new Date() 210 | await new Promise((resolve, reject) => { 211 | user.save(err => { 212 | if (err) { 213 | reject(err) 214 | } 215 | resolve() 216 | }) 217 | }) 218 | $util.sendSuccess(ctx, `Successfully Activated`) 219 | } catch (err) { 220 | throw err 221 | } 222 | } 223 | 224 | exports.requestResetPwd = async (ctx, next) => { 225 | const requestBody = ctx.request.body 226 | let user = await UserModel.findOne({ email: requestBody.email }).exec() 227 | if (!user) { 228 | return $util.sendFailure(ctx, 'accountNotRegistered') 229 | } 230 | 231 | let successContent = 'sendEmailSuccess' 232 | if (!requestBody.resetPasswordToken) { 233 | let buf = crypto.randomBytes(20) 234 | user.resetPasswordToken = user._id + buf.toString('hex') 235 | user.resetPasswordExpires = Date.now() + 24 * 3600 * 1000 236 | let link = `${config.clientPath}/reset-pwd?email=${user.email}&resetPasswordToken=${ 237 | user.resetPasswordToken 238 | }` 239 | sendMail({ to: user.email, type: 'reset', link: link }) 240 | } else { 241 | if (requestBody.resetPasswordToken === user.resetPasswordToken) { 242 | user.password = requestBody.password 243 | successContent = `resetPwdSuccess` 244 | } else { 245 | return $util.sendFailure(ctx, 'tokenValidationFailed') 246 | } 247 | } 248 | 249 | try { 250 | await new Promise((resolve, reject) => { 251 | user.save(err => { 252 | if (err) { 253 | reject(err) 254 | } 255 | resolve() 256 | }) 257 | }) 258 | return $util.sendSuccessWithMsg(ctx, successContent, user.email) 259 | } catch (err) { 260 | throw err 261 | } 262 | } 263 | 264 | exports.setProfile = async (ctx, next) => { 265 | const requestBody = ctx.request.body 266 | let user = await $util.findUser({ _id: requestBody._id }) 267 | if (!user) { 268 | return $util.sendFailure(ctx, 'accountNotRegistered') 269 | } else { 270 | let profileList = requestBody.profile 271 | for (let key in profileList) { 272 | user.profile[key] = profileList[key] || '' 273 | } 274 | if (!user.username) { 275 | user.username = requestBody.username 276 | } 277 | await new Promise((resolve, reject) => { 278 | user.save(err => { 279 | if (err) { 280 | reject(err) 281 | } 282 | resolve() 283 | }) 284 | }) 285 | $util.sendSuccess(ctx, 'Nice, Set Successfully') 286 | } 287 | } 288 | 289 | exports.getProfile = async (ctx, next) => { 290 | const requestBody = ctx.request.query 291 | let user = await $util.findUser({ _id: requestBody._id }) 292 | if (!user) { 293 | return $util.sendFailure(ctx, 'accountNotRegistered') 294 | } else { 295 | return $util.sendSuccess(ctx, { 296 | username: user.username, 297 | profile: user.profile, 298 | email: user.email, 299 | role: user.role, 300 | _id: user._id 301 | }) 302 | } 303 | } 304 | 305 | exports.getUserInfo = async (ctx, next) => { 306 | const requestBody = ctx.request.query 307 | let user = await $util.findUser({ username: requestBody.username }) 308 | if (!user) { 309 | return $util.sendFailure(ctx, 'accountNotRegistered') 310 | } else { 311 | return $util.sendSuccess(ctx, { 312 | username: user.username, 313 | profile: user.profile, 314 | number: user.number, 315 | activeTime: user.activeTime, 316 | createdAt: user.createdAt, 317 | _id: user._id 318 | }) 319 | } 320 | } 321 | 322 | exports.updateAvatar = async (ctx, next) => { 323 | const request = ctx.request 324 | let user = await $util.findUser({ username: request.header.username }) 325 | if (!user) { 326 | return $util.sendFailure(ctx, 'accountNotRegistered') 327 | } else { 328 | try { 329 | const avatarPath = await $util.saveAvatarAndGetPath(ctx.response.req, request.header.imgname) 330 | user.profile.avatar = avatarPath 331 | await new Promise((resolve, reject) => { 332 | user.save(err => { 333 | if (err) { 334 | reject(err) 335 | } 336 | resolve() 337 | }) 338 | }) 339 | return $util.sendSuccess(ctx, { 340 | message: '成功更新头像', 341 | path: avatarPath 342 | }) 343 | } catch (err) { 344 | console.log('上传图片失败', err) 345 | return $util.sendFailure(ctx, 'uploadAbatarFail') 346 | } 347 | } 348 | } 349 | 350 | exports.getAllUsers = async (ctx, next) => { 351 | let options = ctx.request.query 352 | let params = { active: options.active } 353 | let sortParam = {} 354 | options.sortTarget ? (sortParam[options.sortTarget] = options.sortType) : '' 355 | 356 | let limitNumber = parseInt(options.pageSize) 357 | let skipNumber = (parseInt(options.pageCount) - 1) * limitNumber 358 | try { 359 | let count = await UserModel.find({ active: options.active }).count() 360 | return await UserModel.find(params) 361 | .sort(sortParam) 362 | .limit(limitNumber) 363 | .skip(skipNumber) 364 | .exec() 365 | .then(async result => { 366 | $util.sendSuccess(ctx, { 367 | data: result, 368 | count: count 369 | }) 370 | }) 371 | } catch (error) { 372 | $util.sendFailure(ctx, null, 'Opps, Something Error :' + error) 373 | } 374 | } 375 | 376 | exports.removeUserById = async (ctx, next) => { 377 | let options = ctx.request.body 378 | let isAdmin = await $util.checkRoleByUserId(options.operatorId, 'Admin') 379 | if (!isAdmin) { 380 | return $util.sendFailure(ctx, null, 'Opps, You do not have permission to control') 381 | } 382 | try { 383 | return await UserModel.remove({ _id: options._id }).then(async result => { 384 | $util.sendSuccess(ctx, result) 385 | }) 386 | } catch (error) { 387 | $util.sendFailure(ctx, null, 'Opps, Something Error :' + error) 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /server/src/controllers/helpCtrl.js: -------------------------------------------------------------------------------- 1 | const $util = require('./../helper/util'), 2 | axios = require('axios'), 3 | // secretConf = require("./../config/secret"), 4 | secretConf = { 5 | appid: '', 6 | secret: '' 7 | }, 8 | sha1 = require('sha1') 9 | 10 | const getAccessToken = () => { 11 | return new Promise((resolve, reject) => { 12 | const baseUrl = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&' 13 | const appid = secretConf.appid 14 | const secret = secretConf.secret 15 | const requestUrl = baseUrl + `appid=${appid}&secret=${secret}` 16 | console.log('Current getAccessToken requestUrl is: ', requestUrl) 17 | return axios 18 | .get(requestUrl) 19 | .then(result => { 20 | resolve(result.data) 21 | }) 22 | .catch(err => { 23 | console.log('🐛 Opps, Axios Error Occurred !' + err) 24 | resolve({}) 25 | }) 26 | }) 27 | } 28 | 29 | const getWechatTicket = params => { 30 | return new Promise((resolve, reject) => { 31 | const baseUrl = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?&type=jsapi' 32 | const requestUrl = baseUrl + `&access_token=${params.access_token}` 33 | console.log('Current requestUrl is: ', requestUrl) 34 | return axios 35 | .get(requestUrl) 36 | .then(res => { 37 | resolve(res.data) 38 | }) 39 | .catch(err => { 40 | console.log('🐛 Opps, Axios Error Occurred !' + err) 41 | resolve({}) 42 | }) 43 | }) 44 | } 45 | 46 | exports.getWechatApiSignature = async (ctx, next) => { 47 | const url = ctx.request.query.url 48 | const requestParam = await getAccessToken() 49 | const result = await getWechatTicket(requestParam) 50 | const noncestr = $util.generateRandomStr(16) 51 | const timestamp = new Date().getTime() 52 | const signatureFields = [ 53 | `jsapi_ticket=${result.ticket}`, 54 | `noncestr=${noncestr}`, 55 | `timestamp=${timestamp}`, 56 | `url=${url}` 57 | ] 58 | const signatureStr = signatureFields.join('&') 59 | console.log('💯 Current SignatureStr Is: ', signatureStr) 60 | const signature = sha1(signatureStr) 61 | 62 | return $util.sendSuccess(ctx, { 63 | appId: secretConf.appid, 64 | timestamp: timestamp, 65 | nonceStr: noncestr, 66 | signature: signature 67 | }) 68 | } 69 | 70 | exports.crawlLinksInfo = async (ctx, next) => { 71 | let options = ctx.request.query 72 | try { 73 | return await $util.getWebPageInfo(options.url).then(result => { 74 | $util.sendSuccess(ctx, result) 75 | }) 76 | } catch (error) { 77 | ctx.status = 500 78 | ctx.body = '🐛 Opps, Something Error :' + error 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server/src/controllers/linksCtrl.js: -------------------------------------------------------------------------------- 1 | let { Links } = require('./../models/index') 2 | let $util = require('./../helper/util') 3 | let _ = require('lodash') 4 | 5 | /*------------------------------api---------------------------*/ 6 | 7 | exports.getNiceLinks = async (ctx, next) => { 8 | let options = $util.getQueryObject(ctx.request.url) 9 | let params = { active: true } 10 | let sortParam = {} 11 | 12 | options._id ? (params._id = options._id) : '' 13 | options.sortTarget ? (sortParam[options.sortTarget] = options.sortType) : '' 14 | 15 | let limitNumber = parseInt(options.pageSize) 16 | let skipNumber = (parseInt(options.pageCount) - 1) * limitNumber 17 | try { 18 | return await Links.find(params) 19 | .sort(sortParam) 20 | .limit(limitNumber) 21 | .skip(skipNumber) 22 | .exec() 23 | .then(async result => { 24 | /* ----------------------@Add Default----------------------*/ 25 | if (result.length <= 0) { 26 | result.push({ 27 | name: '晚晴幽草', 28 | address: 'https://www.jianshu.com/u/9aae3d8f4c3d', 29 | description: 30 | '产自陕南一隅,流走于深圳的小雄猿。崇文喜武,爱美人尚科技;编码砌字,当下静生活;读思旅行,期成生活静。', 31 | date: '2015-02-05' 32 | }) 33 | } 34 | $util.sendSuccess(ctx, result) 35 | }) 36 | } catch (error) { 37 | $util.sendFailure(ctx, null, 'Opps, Something Error :' + error) 38 | } 39 | } 40 | 41 | exports.addNiceLinks = async (ctx, next) => { 42 | let options = ctx.request.body 43 | if (options.role === 'Admin') { 44 | options.active = await $util.checkRoleByUserId(options.userId, 'Admin') 45 | } 46 | try { 47 | return await Links.create(options).then(async result => { 48 | $util.sendSuccess(ctx, result) 49 | }) 50 | } catch (error) { 51 | if (error.code === 11000) { 52 | return $util.sendFailure(ctx, 'linkHaveBeenAdded') 53 | } 54 | $util.sendFailure(ctx, null, 'Opps, Something Error :' + error) 55 | } 56 | } 57 | 58 | exports.updateNiceLinks = async (ctx, next) => { 59 | let options = ctx.request.body 60 | if (options.managerRole === 'Admin') { 61 | let checkAdmin = await $util.checkRoleByUserId(options.managerId, 'Admin') 62 | if (!checkAdmin) return 63 | } else { 64 | return $util.sendFailure(ctx, null, 'Opps, You do not have permission to control') 65 | } 66 | try { 67 | const user = await $util.findUser({ username: options.createdBy }) 68 | return await Links.update({ _id: options._id }, { $set: options }).then(async result => { 69 | $util.sendSuccess(ctx, result) 70 | }) 71 | } catch (error) { 72 | $util.sendFailure(ctx, null, 'Opps, Something Error :' + error) 73 | } 74 | } 75 | 76 | exports.deleteNiceLinks = async (ctx, next) => { 77 | let options = ctx.request.body 78 | let isAdmin = await $util.checkRoleByUserId(options.operatorId, 'Admin') 79 | if (!isAdmin) { 80 | return $util.sendFailure(ctx, null, 'Opps, You do not have permission to control') 81 | } 82 | try { 83 | return await Links.remove({ _id: options._id }).then(async result => { 84 | $util.sendSuccess(ctx, result) 85 | }) 86 | } catch (error) { 87 | $util.sendFailure(ctx, null, 'Opps, Something Error :' + error) 88 | } 89 | } 90 | 91 | exports.getAllLinks = async (ctx, next) => { 92 | let options = ctx.request.query 93 | let params = { active: options.active } 94 | let sortParam = {} 95 | options.sortTarget ? (sortParam[options.sortTarget] = options.sortType) : '' 96 | 97 | let limitNumber = parseInt(options.pageSize) 98 | let skipNumber = (parseInt(options.pageCount) - 1) * limitNumber 99 | try { 100 | return await Links.find(params) 101 | .sort(sortParam) 102 | .limit(limitNumber) 103 | .skip(skipNumber) 104 | .exec() 105 | .then(async result => { 106 | $util.sendSuccess(ctx, result) 107 | }) 108 | } catch (error) { 109 | $util.sendFailure(ctx, null, 'Opps, Something Error :' + error) 110 | } 111 | } 112 | 113 | exports.getAllLinksCount = async (ctx, next) => { 114 | try { 115 | let options = ctx.request.query 116 | let params = { active: options.active } 117 | let count = await Links.find(params).count() 118 | $util.sendSuccess(ctx, count) 119 | } catch (error) { 120 | $util.sendFailure(ctx, null, 'Opps, Something Error :' + error) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /server/src/helper/errorMsgConf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | accountNotRegistered: { 3 | zh: '尚未找到对应于此邮箱的帐户,请检查。', 4 | en: 'The corresponding account for this mailbox has not been found. Please check it.' 5 | }, 6 | wrongAccountOrPwd: { 7 | zh: '用户名或密码错误。', 8 | en: 'Incorrect username or password.' 9 | }, 10 | accountNoActive: { 11 | zh: '此账号尚未激活,已重新发送邮件至您的邮箱,请及时激活。', 12 | en: 13 | 'This account has not been activated yet, The mail has been sent to your mailbox again. Please activate it in time.' 14 | }, 15 | nameHadRegistered: { 16 | zh: '该名称已被注册。', 17 | en: 'The name has been registered.' 18 | }, 19 | activeValidationFailed: { 20 | zh: `验证失败,验证链接无效或已经过期,请重新 注册。`, 21 | en: `Validation failed. The validation link is invalid or has expired. Please re register` 22 | }, 23 | tokenValidationFailed: { 24 | zh: 'Token 验证失败,或已过期', 25 | en: 'Token Validation Failed,Or expired' 26 | }, 27 | mailboxHadRegistered: { 28 | zh: '您填写的邮箱已经注册了。', 29 | en: 'The mailbox you filled in has been registered.' 30 | }, 31 | pleaseActiveMailbox: { 32 | zh: '您填写的邮箱已经注册了,请激活。', 33 | en: 'The mailbox you filled in has been registered,Please activate it.' 34 | }, 35 | noPassword: { 36 | zh: '您必须输入密码。', 37 | en: 'You must enter a password.' 38 | }, 39 | noUsername: { 40 | zh: '您必须输入用户名。', 41 | en: 'You must enter an username.' 42 | }, 43 | noMailbox: { 44 | zh: '您必须输入电子邮件地址。', 45 | en: 'You must enter an email address.' 46 | }, 47 | linkHaveBeenAdded: { 48 | zh: '该链接已被添加至数据库。', 49 | en: 'The Link Have Been Added.' 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/src/helper/logger.js: -------------------------------------------------------------------------------- 1 | let winston = require('winston') 2 | let fs = require('fs-extra') 3 | let { join, dirname } = require('path') 4 | let $util = require('./../helper/util') 5 | 6 | let projectName = 'awesome-webapp' 7 | let currentDate = $util.formatDate(new Date(), 'YYYY-MM-DD') 8 | let filename = join(__dirname, `./../../logs/${currentDate}.log`) 9 | 10 | let env = process.env.NODE_ENV 11 | if (env === 'production') { 12 | filename = `/data/logs/${projectName}/prod/${currentDate}.log` 13 | } else if (env === 'testing') { 14 | filename = `/data/logs/${projectName}/beta/${currentDate}.log` 15 | } 16 | 17 | fs.ensureDirSync(dirname(filename)) 18 | 19 | let logger = new winston.Logger({ 20 | transports: [new winston.transports.Console(), new winston.transports.File({ filename })] 21 | }) 22 | 23 | module.exports = logger 24 | logger.info('logger start: ', { env, filename }) 25 | -------------------------------------------------------------------------------- /server/src/helper/nodemailer.js: -------------------------------------------------------------------------------- 1 | let nodemailer = require('nodemailer'), 2 | path = require('path'), 3 | fs = require('fs'), 4 | // secretConf = require("./../config/secret") 5 | secretConf = { 6 | email_qq: '', 7 | email_163: '' 8 | } 9 | 10 | let typeList = { 11 | active: { 12 | title: '激活您在「倾城之链」的专属账户', 13 | desc: 14 | '欢迎您加入倾城之链 | NICE LINKS,为了保证正常使用,请在 48 小时内,点击以按钮完成邮件验证,以激活账户。', 15 | button: '激活账户' 16 | }, 17 | reset: { 18 | title: '重设您在「倾城之链」的登录密码', 19 | desc: 20 | '如果您忘记了密码,可点击下面的链接来重置密码;愿您在倾城之链 | NICE LINKS。', 21 | button: '重设密码' 22 | }, 23 | notice: { 24 | title: '来自「倾城之链」的温馨提醒', 25 | desc: 26 | 'Congratulations, 您提交于倾城之链 | NICE LINKS的优质站点,已被授意通过,您可以点击下面的按钮前往查看。', 27 | button: '前往访问' 28 | } 29 | } 30 | 31 | let mailTemp = fs.readFileSync(path.join(__dirname, './../../views/mailTemp.html'), { 32 | encoding: 'utf-8' 33 | }) 34 | 35 | let sendMail = (params = {}) => { 36 | const htmlBody = mailTemp 37 | .replace('#DESC#', typeList[params.type].desc) 38 | .replace('#BUTTON#', typeList[params.type].button) 39 | .replace('#LINK#', params.link) 40 | 41 | const subject = typeList[params.type].title 42 | 43 | // 对于是使用“QQ”邮箱注册用户,则使用"QQ"邮箱发送激活邮件;其他则 163 邮箱; 44 | const isQQRegister = params.to.indexOf('@qq.com') > -1 45 | const authConf = isQQRegister ? secretConf.email_qq : secretConf.email_163 46 | 47 | const smtpTransport = nodemailer.createTransport({ 48 | host: isQQRegister ? 'smtp.qq.com' : 'smtp.163.com', 49 | secure: true, 50 | auth: { 51 | user: authConf.account, 52 | pass: authConf.password 53 | } 54 | }) 55 | 56 | smtpTransport.sendMail( 57 | { 58 | from: params.from || `倾城之链<${authConf.account}>`, 59 | to: params.to, 60 | subject: subject, 61 | html: htmlBody || 'https://nicelinks.site' 62 | }, 63 | function(err, res) { 64 | if (err) { 65 | console.log(err, res) 66 | } else { 67 | params.callback && params.callback() 68 | } 69 | } 70 | ) 71 | } 72 | 73 | module.exports = sendMail 74 | -------------------------------------------------------------------------------- /server/src/helper/successMsgConf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sendEmailSuccess: { 3 | zh: '已发送邮件至 @# 请在 48 小时内按照邮件提示激活。', 4 | en: 5 | 'An e-mail has been sent to @# please within 48 hours in accordance with the message prompts activation.' 6 | }, 7 | resetPwdSuccess: { 8 | zh: '成功重新设置密码', 9 | en: 'Successfully reset the password' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/src/helper/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | DESC:对Date的扩展,将 Date 转化为指定格式的String。 3 | 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符, 4 | 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字) 例子: 5 | (new Date()).Format("YYYY-MM-DD hh:mm:ss.S") ==> 2006-07-02 08:09:04.423 6 | (new Date()).Format("YYYY-M-D h:m:s.S") ==> 2006-7-2 8:9:4.18 7 | */ 8 | const axios = require('axios') 9 | const cheerio = require('cheerio') 10 | const _ = require('lodash') 11 | const fs = require('fs') 12 | const path = require('path') 13 | const mongoSanitize = require('mongo-sanitize') 14 | const formidable = require('formidable') 15 | const Url = require('url') 16 | 17 | const errorMsgConfig = require('./errorMsgConf.js') 18 | const successMsgConfig = require('./successMsgConf.js') 19 | const { UserModel } = require('./../models/index') 20 | const config = require('./../config') 21 | 22 | // 原有的 mongoSanitize 不递归过滤; 23 | function mongoSanitizeRecurse(obj) { 24 | mongoSanitize(obj) 25 | _.each(obj, v => { 26 | if (_.isObject(v)) { 27 | mongoSanitizeRecurse(v) 28 | } 29 | }) 30 | } 31 | 32 | Date.prototype.Format = function(fmt) { 33 | var o = { 34 | 'M+': this.getMonth() + 1, 35 | 'D+': this.getDate(), 36 | 'h+': this.getHours(), 37 | 'm+': this.getMinutes(), 38 | 's+': this.getSeconds(), 39 | 'q+': Math.floor((this.getMonth() + 3) / 3), 40 | S: this.getMilliseconds() 41 | } 42 | if (/(Y+)/.test(fmt)) 43 | fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length)) 44 | for (var k in o) { 45 | if (new RegExp('(' + k + ')').test(fmt)) { 46 | fmt = fmt.replace( 47 | RegExp.$1, 48 | RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length) 49 | ) 50 | } 51 | } 52 | return fmt 53 | } 54 | 55 | module.exports = { 56 | // 安全过滤ctx.query/ctx.request.body等 57 | sanitize(obj) { 58 | mongoSanitizeRecurse(obj) 59 | }, 60 | 61 | sendSuccess(ctx, result, isUpdateRedis = true) { 62 | ctx.statstatusus = 200 63 | ctx.body = { 64 | success: true, 65 | value: result 66 | } 67 | /* 68 | @desc: 使用 Redis 将 GET 请求做缓存, 以提升效率 & 减少数据库压力; 69 | @date: 2018-03-24 70 | */ 71 | const cacheKey = ctx.request.cacheKey 72 | if (config.isOpenRedisFlag && isUpdateRedis && cacheKey && !this.isInRedisIgnoreList(ctx)) { 73 | const ApiCache = require('./../services/apiCache').ApiCache 74 | ApiCache.set(ctx.request.cacheKey, ctx.body) 75 | } 76 | }, 77 | 78 | sendSuccessWithMsg(ctx, signStr, extraParam) { 79 | let msgVal = successMsgConfig[signStr]['zh'] 80 | msgVal = extraParam ? msgVal.replace('@#', extraParam) : msgVal 81 | ctx.status = 200 82 | ctx.body = { 83 | success: true, 84 | value: msgVal 85 | } 86 | }, 87 | 88 | sendFailure(ctx, signStr, errMsg) { 89 | ctx.body = { 90 | success: false, 91 | message: signStr ? errorMsgConfig[signStr]['zh'] : errMsg 92 | } 93 | }, 94 | 95 | // findUser:params === {} 即获得所有用户信息; 96 | findUser(params = {}) { 97 | return new Promise((resolve, reject) => { 98 | UserModel.findOne(params, (err, doc) => { 99 | if (err) { 100 | reject(err) 101 | } 102 | resolve(doc) 103 | }) 104 | }) 105 | }, 106 | 107 | findUserIdByUsername(username) { 108 | return new Promise((resolve, reject) => { 109 | UserModel.findOne({ username: username }, (err, foundUser) => { 110 | if (err) { 111 | reject(err) 112 | } 113 | resolve(foundUser._id) 114 | }) 115 | }) 116 | }, 117 | 118 | // Role authorization check 119 | checkRoleByUserId(userId, role) { 120 | return new Promise((resolve, reject) => { 121 | try { 122 | UserModel.findOne({ _id: userId }, (err, foundUser) => { 123 | if (err) { 124 | return reject(err) 125 | } 126 | if (foundUser.role === role) { 127 | return resolve(true) 128 | } else { 129 | return resolve(false) 130 | } 131 | }) 132 | } catch (error) { 133 | return $util.sendFailure(ctx, null, 'Opps, Something Error :' + error) 134 | } 135 | }) 136 | }, 137 | 138 | query(search) { 139 | let str = search || window.location.search 140 | let objURL = {} 141 | 142 | str.replace(new RegExp('([^?=&]+)(=([^&]*))?', 'g'), ($0, $1, $2, $3) => { 143 | objURL[$1] = $3 144 | }) 145 | return objURL 146 | }, 147 | 148 | formatDate(date, rule = 'YYYY-MM-DD') { 149 | return (date && date.Format(rule)) || '' 150 | }, 151 | 152 | verifyIsLegalEmail(str) { 153 | const pattern = new RegExp('^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$', 'g') 154 | return pattern.test(str) 155 | }, 156 | 157 | verifyUserIdEffective(userId) { 158 | let regExp = new RegExp('^[A-Za-z0-9]{24}$|^[A-Za-z0-9]{32}$') 159 | return regExp.test(userId) 160 | }, 161 | 162 | queryString(url, query) { 163 | let str = [] 164 | for (let key in query) { 165 | str.push(key + '=' + query[key]) 166 | } 167 | return url + '?' + str.join('&') 168 | }, 169 | 170 | // 获取当前地址,指定参数的值; 171 | getUrlParam(url, name) { 172 | var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)') 173 | var r = url.search.substr(1).match(reg) 174 | if (r != null) { 175 | return unescape(r[2]) 176 | } 177 | return null 178 | }, 179 | 180 | easyCompressString(str = '', factor = 9) { 181 | str = str.toString() 182 | let result = 0 183 | for (var i = 0; i < str.length; i++) { 184 | result += str.charCodeAt(i) * factor 185 | } 186 | return result 187 | }, 188 | 189 | getRedisCacheKey(ctx) { 190 | const request = ctx.request 191 | const originUrlParsed = Url.parse(request.originalUrl) 192 | 193 | const cachePath = originUrlParsed ? originUrlParsed.pathname : '' 194 | const cacheParam = this.easyCompressString(originUrlParsed.path, 88) 195 | const cacheKey = `${cachePath}-${cacheParam}` 196 | console.log(`Current cacheKey is : ${cacheKey}`) 197 | return cacheKey 198 | }, 199 | 200 | isInRedisIgnoreList(ctx) { 201 | const ignoreApiList = ['crawlLinksInfo', 'getNiceLinks', 'getUserInfo', 'getProfile'] 202 | const currentUrl = ctx.request.url 203 | let isInIgnoreListFlag = false 204 | ignoreApiList.forEach(element => { 205 | if (currentUrl.indexOf(element) > -1) { 206 | isInIgnoreListFlag = true 207 | } 208 | }) 209 | return isInIgnoreListFlag 210 | }, 211 | 212 | getQueryObject(queryStr) { 213 | var str = queryStr === undefined ? location.search : queryStr 214 | var obj = {} 215 | var reg = /([^?&=]+)=([^?&=]*)/g 216 | str.replace(reg, function(match, $1, $2) { 217 | var name = decodeURIComponent($1) 218 | var val = decodeURIComponent($2) 219 | obj[name] = val 220 | }) 221 | return obj 222 | }, 223 | 224 | getWebPageInfo(url) { 225 | return new Promise((resolve, reject) => { 226 | return axios 227 | .get(url) 228 | .then(res => { 229 | try { 230 | let $ = cheerio.load(res.data) 231 | let description = $('meta[name="description"]').attr('content') 232 | let keywords = $('meta[name="keywords"]').attr('content') 233 | let result = { 234 | title: $('title').text() || $('meta[og:title"]').attr('content'), 235 | keywords: keywords || $('meta[property="og:keywords"]').attr('content') || '', 236 | desc: description || $('meta[property="og:description"]').attr('content') 237 | } 238 | resolve(result) 239 | } catch (err) { 240 | console.log('Opps, Download Error Occurred !' + err) 241 | resolve({}) 242 | } 243 | }) 244 | .catch(err => { 245 | console.log('Opps, Axios Error Occurred !' + err) 246 | resolve({}) 247 | }) 248 | }) 249 | }, 250 | 251 | generateRandomStr(length = 16) { 252 | let randomStr = '' 253 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 254 | const possibleLen = possible.length 255 | for (let i = 0; i < length; i++) { 256 | randomStr += possible.charAt(Math.floor(Math.random() * possibleLen)) 257 | } 258 | return randomStr 259 | }, 260 | 261 | async saveAvatarAndGetPath(req, imgName) { 262 | return new Promise((resolve, reject) => { 263 | const form = formidable.IncomingForm() 264 | form.uploadDir = config.main.avatarUploadDir 265 | try { 266 | form.parse(req, async (err, fields, files) => { 267 | console.log(err, fields, files) 268 | const fullName = imgName + path.extname(files.file.name) 269 | const repath = config.main.avatarUploadDir + fullName 270 | await fs.rename(files.file.path, repath) 271 | return resolve(fullName) 272 | }) 273 | } catch (err) { 274 | fs.unlink(files.file.path) 275 | return reject('保存图片失败') 276 | } 277 | }) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./app') 2 | -------------------------------------------------------------------------------- /server/src/middlewares/cache.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs') 2 | let path = require('path') 3 | const ApiCache = require('./../services/apiCache').ApiCache 4 | const $util = require('./../helper/util') 5 | let config = require('./../config') 6 | 7 | const getServiceWorker = () => { 8 | let filePath = __dirname + '/../../public/service-worker.js' 9 | let content = fs.readFileSync(filePath, 'utf8') 10 | global.serviceWorkerContent = content 11 | return content 12 | } 13 | 14 | exports.RedisCache = async function(ctx, next) { 15 | const request = ctx.request 16 | const isRequestApi = request.url.indexOf('/api/') > -1 17 | const isRequestSource = request.url.indexOf('/static/') > -1 18 | 19 | console.log('request.url', request.url) 20 | if (request.url === '/service-worker.js') { 21 | ctx.body = global.serviceWorkerContent || getServiceWorker() 22 | return 23 | } 24 | 25 | if (!isRequestApi && !isRequestSource) { 26 | if (global.indexPageContent) { 27 | ctx.body = global.indexPageContent 28 | return 29 | } 30 | let filePath = __dirname + '/../../public/index.html' 31 | let content = fs.readFileSync(filePath, 'utf8') 32 | ctx.body = content 33 | global.indexPageContent = content 34 | return 35 | } 36 | 37 | if ( 38 | isRequestApi && 39 | config.isOpenRedisFlag && 40 | request.method === 'GET' && 41 | !$util.isInRedisIgnoreList(ctx) 42 | ) { 43 | // 设置 cacheKey 以便在获得数据库的结果处,将数据依据此 key 存入此 Redis; 44 | const cacheKey = $util.getRedisCacheKey(ctx) 45 | ctx.request.cacheKey = cacheKey 46 | try { 47 | const respResult = await ApiCache.get(cacheKey) 48 | if (respResult) { 49 | console.log(`✔ Get Api Result From Cache Success.`) 50 | return $util.sendSuccess(ctx, respResult.value, false) 51 | } 52 | } catch (error) { 53 | console.log(`❌ Get Api Cache Error: `, error) 54 | } 55 | } 56 | await require('./../routes').routes()(ctx, next) 57 | } 58 | -------------------------------------------------------------------------------- /server/src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | let cors = require('koa2-cors') 3 | let json = require('koa-json') 4 | let views = require('koa-views') 5 | let convert = require('koa-convert') 6 | let KoaStatic = require('koa-static') 7 | let KoaHelmet = require('koa-helmet') 8 | let KoaMount = require('koa-mount') 9 | let KoaSession = require('koa-session2') 10 | let KoaRedis = require('koa-redis') 11 | let Bodyparser = require('koa-bodyparser') 12 | 13 | let bodyparser = Bodyparser() 14 | 15 | let config = require('./../config') 16 | 17 | function applyMiddleware(app) { 18 | app.use( 19 | cors({ 20 | origin: '*', 21 | maxAge: 1000, 22 | credentials: true, 23 | allowMethods: ['GET', 'POST', 'DELETE'], 24 | allowHeaders: ['Content-Type', 'Authorization', 'Accept'] 25 | }) 26 | ) 27 | 28 | app.use(convert(bodyparser)) 29 | app.use(convert(json())) 30 | app.use(KoaHelmet()) 31 | 32 | // 替换'x-koa-redis-cache' 为'x-server-cache' 同helmet信息隐藏 33 | app.use(async (ctx, next) => { 34 | await next() 35 | if (ctx.response.get('x-koa-redis-cache')) { 36 | ctx.remove('x-koa-redis-cache') 37 | ctx.set('x-server-cache', true) 38 | } 39 | }) 40 | 41 | app.proxy = true 42 | app.use( 43 | KoaSession({ 44 | store: new KoaRedis(config.main.redis.session), 45 | key: 'SESSIONID', 46 | maxAge: 86400000, 47 | overwrite: true, 48 | httpOnly: true, 49 | signed: true 50 | }) 51 | ) 52 | app.use(config.passport.initialize()) 53 | app.use(config.passport.session()) 54 | 55 | app.use(require('./cache').RedisCache) 56 | 57 | // handle static 58 | app.use( 59 | convert( 60 | KoaStatic(path.join(__dirname, '../../public'), { 61 | pathPrefix: '' 62 | }) 63 | ) 64 | ) 65 | 66 | app.use(KoaMount('/api/avatar', KoaStatic(config.main.avatarUploadDir))) 67 | 68 | // handle views 69 | app.use( 70 | views(path.join(__dirname, '../../views'), { 71 | extension: 'ejs' 72 | }) 73 | ) 74 | 75 | // handle 404 76 | app.use(async ctx => { 77 | ctx.status = 404 78 | await ctx.render('404') 79 | }) 80 | 81 | // hanle logger 82 | app.use(async (ctx, next) => { 83 | const start = new Date() 84 | await next() 85 | const ms = new Date() - start 86 | console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) 87 | }) 88 | } 89 | 90 | exports.applyMiddleware = applyMiddleware 91 | -------------------------------------------------------------------------------- /server/src/models/index.js: -------------------------------------------------------------------------------- 1 | let mongoose = require('mongoose') 2 | let config = require('./../config') 3 | 4 | /* Solve Problem: (node) DeprecationWarning: 5 | Mongoose: mpromise (mongoose's default promise library) is deprecated 6 | */ 7 | mongoose.Promise = global.Promise 8 | 9 | function connectDatabase(uri) { 10 | return new Promise((resolve, reject) => { 11 | mongoose.connection 12 | .on('error', error => reject(error)) 13 | .on('close', () => console.log('Database connection closed.')) 14 | .once('open', () => resolve(mongoose.connections[0])) 15 | mongoose.connect(uri) 16 | }) 17 | } 18 | 19 | connectDatabase(config.main.database) 20 | 21 | exports.Links = require('./linksModel') 22 | exports.UserModel = require('./userModel') 23 | -------------------------------------------------------------------------------- /server/src/models/linksModel.js: -------------------------------------------------------------------------------- 1 | let mongoose = require('mongoose'), 2 | Schema = mongoose.Schema, 3 | ObjectId = mongoose.Schema.ObjectId 4 | 5 | /* Solve Problem: (node) DeprecationWarning: 6 | Mongoose: mpromise (mongoose's default promise library) is deprecated 7 | */ 8 | mongoose.Promise = global.Promise 9 | 10 | // 定义 LinksSchema 数据表和数据结构 11 | const LinksSchema = new mongoose.Schema({ 12 | path: { 13 | type: String, 14 | lowercase: true, 15 | unique: true, 16 | required: true 17 | }, 18 | title: { 19 | type: String, 20 | required: true 21 | }, 22 | desc: { 23 | type: String, 24 | default: '' 25 | }, 26 | keywords: { 27 | type: String, 28 | default: '' 29 | }, 30 | active: { 31 | type: Boolean, 32 | default: false 33 | }, 34 | created: { 35 | type: Date, 36 | default: Date.now 37 | }, 38 | updated: { 39 | type: Date, 40 | default: Date.now 41 | } 42 | }) 43 | 44 | module.exports = mongoose.model('Links', LinksSchema) 45 | -------------------------------------------------------------------------------- /server/src/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'), 2 | Schema = mongoose.Schema, 3 | bcrypt = require('bcrypt-nodejs') 4 | 5 | // ================================ 6 | // User Schema 7 | // ================================ 8 | const UserSchema = new Schema( 9 | { 10 | email: { 11 | type: String, 12 | lowercase: true, 13 | unique: true, 14 | required: true 15 | }, 16 | username: { 17 | type: String, 18 | unique: true, 19 | required: true 20 | }, 21 | password: { 22 | type: String, 23 | required: true 24 | }, 25 | profile: { 26 | nickname: { type: String, default: '' }, 27 | website: { type: String, default: '' }, 28 | description: { type: String, default: '' }, 29 | avatar: { type: String, default: '' } 30 | }, 31 | role: { 32 | type: String, 33 | enum: ['Member', 'Owner', 'Admin'], 34 | default: 'Member' 35 | }, 36 | registeTime: { 37 | type: Date 38 | }, 39 | activeTime: { 40 | type: Date 41 | }, 42 | number: { 43 | type: Number, 44 | default: 1 45 | }, 46 | resetPasswordToken: { 47 | type: String 48 | }, 49 | resetPasswordExpires: { 50 | type: Date 51 | }, 52 | active: { 53 | type: Boolean, 54 | default: false 55 | }, 56 | activeToken: String, 57 | activeExpires: Date 58 | }, 59 | { 60 | timestamps: true 61 | } 62 | ) 63 | 64 | // Pre-save of user to database, hash password if password is modified or new 65 | UserSchema.pre('save', function(next) { 66 | const user = this, 67 | SALT_FACTOR = 5 68 | 69 | if (!user.isModified('password')) return next() 70 | 71 | bcrypt.genSalt(SALT_FACTOR, function(err, salt) { 72 | if (err) return next(err) 73 | 74 | bcrypt.hash(user.password, salt, null, function(err, hash) { 75 | if (err) return next(err) 76 | user.password = hash 77 | next() 78 | }) 79 | }) 80 | }) 81 | 82 | // Method to compare password for login 83 | UserSchema.methods.comparePassword = function(candidatePassword, cb) { 84 | bcrypt.compare(candidatePassword, this.password, function(err, isMatch) { 85 | if (err) { 86 | return cb(err) 87 | } 88 | 89 | cb(null, isMatch) 90 | }) 91 | } 92 | 93 | module.exports = mongoose.model('UserModel', UserSchema) 94 | -------------------------------------------------------------------------------- /server/src/routes/index.js: -------------------------------------------------------------------------------- 1 | let Router = require('koa-router') 2 | let LinksCtrl = require('../controllers/linksCtrl') 3 | let AuthController = require('../controllers/authCtrl') 4 | let HelpController = require('../controllers/helpCtrl') 5 | 6 | let $util = require('../helper/util') 7 | let fs = require('fs') 8 | let { join } = require('path') 9 | 10 | const router = Router({ 11 | prefix: '/api' 12 | }) 13 | 14 | router.use(async (ctx, next) => { 15 | $util.sanitize(ctx.query) 16 | await next() 17 | }) 18 | 19 | // api cors 20 | router.use(async (ctx, next) => { 21 | ctx.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 22 | ctx.set('Access-Control-Allow-Origin', '*') 23 | ctx.set('Access-Control-Allow-Credentials', true) 24 | ctx.set('Access-Control-Allow-Origin', '*') 25 | await next() 26 | }) 27 | 28 | // api options method 29 | router.options('*', async (ctx, next) => { 30 | ctx.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 31 | ctx.set('Access-Control-Allow-Origin', '*') 32 | ctx.status = 200 33 | await next() 34 | }) 35 | 36 | router.get('/index', async (ctx, next) => { 37 | let indexPage = join(__dirname, '../../public/index.html') 38 | let content = await fs.readFileSync(indexPage, 'utf-8') 39 | ctx.body = content 40 | }) 41 | 42 | router.get('/getNiceLinks', LinksCtrl.getNiceLinks) 43 | 44 | router.post('/addNiceLinks', LinksCtrl.addNiceLinks) 45 | 46 | router.post('/updateNiceLinks', LinksCtrl.updateNiceLinks) 47 | 48 | router.post('/deleteNiceLinks', LinksCtrl.deleteNiceLinks) 49 | 50 | router.get('/getAllLinks', LinksCtrl.getAllLinks) 51 | 52 | router.get('/getAllLinksCount', LinksCtrl.getAllLinksCount) 53 | // *********************Login Auth Register********************** Strat// 54 | 55 | // Registration route 56 | router.post('/checkIsExisted', AuthController.checkIsExisted) 57 | 58 | router.post('/signup', AuthController.signup) 59 | 60 | // Login router 61 | router.post('/login', AuthController.login) 62 | 63 | // logout router 64 | router.post('/logout', AuthController.logout) 65 | 66 | // logoff router 67 | router.post('/logoff', AuthController.logoff) 68 | 69 | router.post('/active', AuthController.active) 70 | 71 | router.post('/requestResetPwd', AuthController.requestResetPwd) 72 | 73 | router.post('/setProfile', AuthController.setProfile) 74 | 75 | router.get('/getProfile', AuthController.getProfile) 76 | 77 | router.get('/getUserInfo', AuthController.getUserInfo) 78 | 79 | router.post('/updateAvatar', AuthController.updateAvatar) 80 | 81 | router.get('/getAllUsers', AuthController.getAllUsers) 82 | 83 | router.post('/removeUserById', AuthController.removeUserById) 84 | // router.use('/auth', authRoutes.routes()) 85 | 86 | // --------------------------Help--------------------------- 87 | router.get('/crawlLinksInfo', HelpController.crawlLinksInfo) 88 | 89 | router.get('/getWechatApiSignature', HelpController.getWechatApiSignature) 90 | 91 | module.exports = router 92 | -------------------------------------------------------------------------------- /server/src/services/apiCache.js: -------------------------------------------------------------------------------- 1 | const Bluebird = require('bluebird') 2 | const RedisCache = require('./redisCache').RedisCache 3 | const config = require('./../config') 4 | const redisConfig = config.main.redis 5 | 6 | /* 7 | const zlib = Bluebird.promisifyAll(require('zlib')); 8 | const gzip = (val) => { 9 | return zlib.gzipAsync(val) 10 | } 11 | 12 | const unzip = (binary) => { 13 | return zlib.unzipAsync(binary) 14 | } 15 | */ 16 | 17 | const stringify = string => { 18 | return new Bluebird((resolve, reject) => { 19 | try { 20 | resolve(JSON.stringify(string)) 21 | } catch (err) { 22 | reject(err) 23 | } 24 | }) 25 | } 26 | 27 | const parse = string => { 28 | return new Bluebird((resolve, reject) => { 29 | try { 30 | resolve(JSON.parse(string)) 31 | } catch (err) { 32 | reject(err) 33 | } 34 | }) 35 | } 36 | 37 | const convert = buffer => { 38 | return new Bluebird((resolve, reject) => { 39 | try { 40 | resolve(buffer.toString()) 41 | } catch (err) { 42 | reject(err) 43 | } 44 | }) 45 | } 46 | 47 | class ApiCache extends RedisCache { 48 | constructor(settings) { 49 | super(settings) 50 | } 51 | 52 | async set(key, val) { 53 | let newVal = await stringify(val) 54 | return super.set(key, newVal) 55 | } 56 | 57 | async setExpire(key, length = 180) { 58 | super.setExpire(key, length) 59 | } 60 | 61 | async get(key) { 62 | const result = await super.get(key) 63 | if (!result) { 64 | return new Bluebird((resolve, reject) => { 65 | resolve(null) 66 | }) 67 | } 68 | 69 | return super 70 | .get(key) 71 | .then(buffer => convert(buffer)) 72 | .then(json => parse(json)) 73 | } 74 | } 75 | 76 | exports.ApiCache = new ApiCache({ 77 | options: { 78 | host: redisConfig.client.host, 79 | port: redisConfig.client.port || 39, 80 | db: redisConfig.client.db || 0, 81 | return_buffers: true 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /server/src/services/pageCache.js: -------------------------------------------------------------------------------- 1 | const Bluebird = require('bluebird') 2 | const RedisCache = require('./redisCache').RedisCache 3 | const config = require('./../config') 4 | const redisConfig = config.main.redis 5 | 6 | class PageCache extends RedisCache { 7 | constructor(settings) { 8 | super(settings) 9 | } 10 | 11 | async set(key, val) { 12 | const result = super.set(key, val) 13 | super.setExpire(key, 7 * 24 * 60 * 60) 14 | return result 15 | } 16 | 17 | async get(key) { 18 | const result = await super.get(key) 19 | if (!result) { 20 | return new Bluebird((resolve, reject) => { 21 | resolve(null) 22 | }) 23 | } 24 | 25 | return super.get(key) 26 | } 27 | } 28 | 29 | exports.PageCache = new PageCache({ 30 | options: { 31 | host: redisConfig.client.host, 32 | port: redisConfig.client.port || 39, 33 | db: redisConfig.client.db || 1, 34 | return_buffers: false 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /server/src/services/redisCache.js: -------------------------------------------------------------------------------- 1 | const Redis = require('redis') 2 | const Bluebird = require('bluebird') 3 | 4 | /* 5 | @desc: Fix Bug: TypeError: this.redisClient.getAsync is not a function 6 | @link: https://github.com/NodeRedis/node_redis#bluebird-promises 7 | @date: 2018-03-24 8 | */ 9 | Bluebird.promisifyAll(Redis.RedisClient.prototype) 10 | 11 | const redisBaseConfig = { 12 | retry_strategy(options) { 13 | console.log(options.error) 14 | if (options.error && options.error.code === 'ECONNREFUSED') { 15 | console.log(`❗️ Error: ${options.error.message}`) 16 | return undefined 17 | } 18 | 19 | if (options.times_connected > 10) { 20 | return undefined 21 | } 22 | 23 | return Math.min(options.attempt * 100, 500) 24 | } 25 | } 26 | 27 | class RedisCache { 28 | constructor(settings) { 29 | this.settings = settings 30 | this.options = settings.options 31 | this.redisClient = null 32 | this.setup() 33 | } 34 | 35 | setup() { 36 | this.redisConfig = Object.assign({}, redisBaseConfig, this.options) 37 | this.redisClient = Redis.createClient(this.redisConfig) 38 | 39 | this.redisClient.on('error', function(error) { 40 | console.error(`❗️ Redis Error: ${error}`) 41 | }) 42 | 43 | this.redisClient.on('ready', () => { 44 | console.log('✅ 💃 redis have ready !') 45 | }) 46 | 47 | this.redisClient.on('connect', () => { 48 | console.log('✅ 💃 connect redis success !') 49 | }) 50 | } 51 | 52 | async set(key, val) { 53 | if (!key || !val) { 54 | const whichOne = !key && !val ? 'Key and val' : !key ? 'key' : 'val' 55 | throw new Error(`❌ ${whichOne} required when set new cache item.`) 56 | } 57 | 58 | if (typeof key !== 'string') { 59 | throw new Error('❌ Expected key is a string.') 60 | } 61 | 62 | try { 63 | const result = this.redisClient.setAsync(key, val) 64 | this.redisClient.expire(key, 180) 65 | if (result.toString() === 'OK') { 66 | return val 67 | } 68 | } catch (error) { 69 | throw new Error(`❌ Set cache failed, key is ${key}`) 70 | } 71 | } 72 | 73 | /** 74 | * @desc 为某条 Key 设置对应的过期时长; 75 | * @param {*} time 单位(S) 默认三分钟. 76 | */ 77 | async setExpire(key, length = 180) { 78 | this.redisClient.expire(key, length) 79 | } 80 | 81 | async get(key) { 82 | if (!key) { 83 | throw new Error('❌ Key is required when fetch value.') 84 | } 85 | 86 | return await this.redisClient.getAsync(key) 87 | } 88 | } 89 | 90 | exports.RedisCache = RedisCache 91 | -------------------------------------------------------------------------------- /server/test/test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import 'should' 5 | import app from '../src' 6 | 7 | describe('HTTP APP TEST', () => { 8 | describe('Koa GET /', () => { 9 | it('should 200', done => { 10 | request(app.listen()) 11 | .get('/') 12 | .set('Accept', 'application/text') 13 | .expect('Content-Type', /text/) 14 | .end((err, res) => { 15 | if (err) { 16 | throw new Error(err) 17 | } 18 | // console.log(res) 19 | res.status.should.equal(200) 20 | // console.log(res.text) 21 | res.text.should.equal( 22 | "\n\n \n koa2 title\n \n \n \n

koa2 title

\n

EJS Welcome to koa2 title

\n \n\n" 23 | ) 24 | done() 25 | }) 26 | }) 27 | }) 28 | 29 | describe('Koa Static, GET /static/stylesheets/style.css', () => { 30 | it('should 200', done => { 31 | const styleCssContent = fs.readFileSync( 32 | path.join(__dirname, '../public/static/stylesheets/style.css'), 33 | 'utf-8' 34 | ) 35 | request(app.listen()) 36 | .get('/static/stylesheets/style.css') 37 | .set('Accept', 'application/text') 38 | .expect('Content-Type', /text/) 39 | .end((err, res) => { 40 | if (err) { 41 | throw new Error(err) 42 | } 43 | // console.log(res) 44 | res.status.should.equal(200) 45 | // console.log(res.text) 46 | res.text.should.equal(styleCssContent) 47 | done() 48 | }) 49 | }) 50 | }) 51 | 52 | describe('GET /pathNotMatchAny', () => { 53 | it('should 404', done => { 54 | request(app.listen()) 55 | .get('/pathNotMatchAny') 56 | .set('Accept', 'application/text') 57 | .expect('Content-Type', /text/) 58 | .end((err, res) => { 59 | if (err) { 60 | throw new Error(err) 61 | } 62 | // console.log(res) 63 | res.status.should.equal(404) 64 | done() 65 | }) 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /server/views/404.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /server/views/422.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /server/views/500.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /server/views/docker-vue-node-nginx-mongodb-redis-dragon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicejade/docker-vue-node-nginx-mongodb-redis/98316b2f6b1ff56ad38a6f412fc402b59f3be93e/server/views/docker-vue-node-nginx-mongodb-redis-dragon.png -------------------------------------------------------------------------------- /server/views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | -------------------------------------------------------------------------------- /server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | 6 | 7 | 8 |

<%= title %>

9 |

EJS Welcome to <%= title %>

10 | 11 | 12 | -------------------------------------------------------------------------------- /server/views/mailTemp.html: -------------------------------------------------------------------------------- 1 |

倾城之链 | NICE LINKS

倾城之链 | NICE LINKS

#DESC#

#BUTTON#

倾城之链,作为一个开放平台,旨在云集全球优秀网站,探索互联网中更广阔的世界;在这里,你可以轻松发现、学习、分享更多有用或有趣的事物。
NICE LINKS, as an open platform, is designed to gather around the world's excellent websites to explore the wider world of the Internet; Here, you can easily find, learn, and share more useful or interesting things.

倾城之链 :云集世间优秀站点
Copyright © 2017-2018 nicelinks.com
Made by 晚晴幽草轩轩主
--------------------------------------------------------------------------------