├── .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 |
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 | |  |  | |
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, 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.
--------------------------------------------------------------------------------