├── .dockerignore ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── admin ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── Dockerfile ├── build │ ├── build.js │ ├── check-versions.js │ ├── dev-client.js │ ├── dev-server.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ ├── webpack.prod.conf.js │ └── webpack.test.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ ├── prod.env.js │ └── test.env.js ├── index.html ├── package.json ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── Main.vue │ │ ├── containers │ │ │ ├── Create.vue │ │ │ ├── List.vue │ │ │ ├── Markdown.vue │ │ │ └── Post.vue │ │ ├── pages │ │ │ ├── AlbumCreate.vue │ │ │ ├── Dashboard.vue │ │ │ ├── Login.vue │ │ │ ├── Logout.vue │ │ │ ├── Sidebar.vue │ │ │ └── Top.vue │ │ ├── utils │ │ │ └── marked.js │ │ └── views │ │ │ ├── CreateEditView.js │ │ │ ├── CreateListView.js │ │ │ └── CreateMarkdownView.js │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ ├── api.js │ │ └── index.js │ └── utils │ │ └── error.js ├── static │ └── .gitkeep └── test │ ├── e2e │ ├── custom-assertions │ │ └── elementCount.js │ ├── nightwatch.conf.js │ ├── runner.js │ └── specs │ │ └── test.js │ └── unit │ ├── .eslintrc │ ├── index.js │ └── karma.conf.js ├── docker-compose.yml ├── front ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── Dockerfile ├── build │ ├── setup-dev-server.js │ ├── vue-loader.config.js │ ├── webpack.base.config.js │ ├── webpack.client.config.js │ └── webpack.server.config.js ├── config │ ├── dev.env.js │ ├── index.js │ ├── prod.env.js │ └── test.env.js ├── index.html ├── middleware │ ├── favicon.js │ └── serverGoogleAnalytic.js ├── package-lock.json ├── package.json ├── pm2.json ├── server.js ├── server │ ├── config.js │ ├── mongo.js │ ├── robots.js │ ├── rss.js │ └── sitemap.js ├── src │ ├── App.vue │ ├── assets │ │ ├── css │ │ │ ├── index.styl │ │ │ ├── mixin.styl │ │ │ └── variable.styl │ │ ├── fonts │ │ │ ├── jackeyhandwrite-webfont.ttf │ │ │ ├── jackeyhandwrite-webfont.woff │ │ │ ├── jackeyhandwrite-webfont.woff2 │ │ │ └── ljh.ttf │ │ └── js │ │ │ └── base.js │ ├── client-entry.ts │ ├── components │ │ ├── About │ │ │ └── index.vue │ │ ├── Archive │ │ │ └── index.vue │ │ ├── Back │ │ │ ├── img │ │ │ │ └── back.svg │ │ │ └── index.vue │ │ ├── CatchMe │ │ │ └── index.vue │ │ ├── Disqus │ │ │ └── index.vue │ │ ├── Footer │ │ │ └── index.vue │ │ ├── Header │ │ │ └── index.vue │ │ ├── Link │ │ │ └── index.vue │ │ ├── Loading │ │ │ └── index.vue │ │ ├── Musicplayer │ │ │ └── index.vue │ │ ├── Pagination │ │ │ └── index.vue │ │ ├── Post │ │ │ └── index.vue │ │ ├── Sidebar │ │ │ └── index.vue │ │ ├── Tag │ │ │ └── index.vue │ │ ├── about │ │ │ └── img │ │ │ │ └── me.svg │ │ ├── album │ │ │ └── index.vue │ │ ├── blog-pager │ │ │ └── index.vue │ │ ├── blog-summary │ │ │ └── index.vue │ │ ├── catchme │ │ │ └── img │ │ │ │ └── catchme.svg │ │ ├── link │ │ │ └── img │ │ │ │ └── link-bg.svg │ │ ├── musicplayer │ │ │ ├── part │ │ │ │ ├── aplayer-controller-progress.vue │ │ │ │ ├── aplayer-controller-volume.vue │ │ │ │ ├── aplayer-controller.vue │ │ │ │ ├── aplayer-icon.vue │ │ │ │ ├── aplayer-iconbutton.vue │ │ │ │ ├── aplayer-list.vue │ │ │ │ ├── aplayer-lrc.vue │ │ │ │ └── aplayer-thumbnail.vue │ │ │ └── utils.js │ │ ├── page-container │ │ │ └── index.vue │ │ ├── post-container │ │ │ └── index.vue │ │ ├── post │ │ │ └── img │ │ │ │ └── post-bg.svg │ │ ├── second-title │ │ │ ├── img │ │ │ │ └── mask.svg │ │ │ └── index.vue │ │ ├── sidebar │ │ │ └── img │ │ │ │ ├── rss.svg │ │ │ │ └── sidebar.svg │ │ └── tag-pager │ │ │ └── index.vue │ ├── main.ts │ ├── mixin │ │ ├── disqus.ts │ │ └── image.ts │ ├── router │ │ └── index.js │ ├── server-entry.ts │ ├── store │ │ ├── create-api-client.js │ │ ├── create-api-server.js │ │ ├── index.js │ │ └── store.js │ ├── typings │ │ ├── index.d.ts │ │ └── vue-shims.d.ts │ ├── utils │ │ ├── 404.js │ │ ├── clientGoogleAnalyse.js │ │ └── utils.js │ └── views │ │ └── CreatePostView.js ├── static │ ├── favicon.ico │ └── title_base.png └── tsconfig.json ├── id_rsa.enc ├── nginx.conf └── server ├── .gitignore ├── Dockerfile ├── app.js ├── blogpack.js ├── build ├── blogpack.base.config.js ├── blogpack.dev.config.js └── blogpack.prod.config.js ├── conf ├── config.js └── option.js ├── model ├── mongo.js └── redis.js ├── mongoRest ├── actions.js ├── index.js └── routes.js ├── package.json ├── plugins ├── beforeRestful │ └── checkAuth.js ├── beforeServerStart │ ├── initOption.js │ ├── initUser.js │ └── installTheme.js ├── beforeUseRoutes │ ├── bodyParser.js │ ├── logTime.js │ ├── ratelimit.js │ └── restc.js └── mountingRoute │ ├── login.js │ ├── logout.js │ └── qiniu.js ├── pm2.json ├── service └── token.js ├── theme └── firekylin.js ├── utils └── log.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | .git/ 3 | .vscode/ 4 | node_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | test/unit/coverage 7 | test/e2e/reports 8 | selenium-debug.log 9 | *.rdb 10 | /dist 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | services: 4 | - docker 5 | node_js: 6 | - '7' 7 | addons: 8 | ssh_known_hosts: 47.100.112.11:3999 9 | cache: 10 | directories: 11 | - front/node_modules 12 | before_install: 13 | - openssl aes-256-cbc -K $encrypted_111bdbda2220_key -iv $encrypted_111bdbda2220_iv 14 | -in id_rsa.enc -out ~/.ssh/id_rsa -d 15 | - chmod 600 ~/.ssh/id_rsa 16 | - cd front 17 | - npm set registry https://registry.npm.taobao.org 18 | install: 19 | - npm install chromedriver --chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/chromedriver 20 | - npm install 21 | script: 22 | - npm run build 23 | after_success: 24 | - cd ~ 25 | - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" 26 | - docker-compose build 27 | - docker-compose push web_front web_back 28 | - ssh unsad@47.100.112.11 -p 3999 "./docker_start.sh" 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 unsad 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 | # ssr-blog 2 | 3 | [![Build Status](https://travis-ci.org/unsad/ssr-blog.svg?branch=master)](https://travis-ci.org/unsad/ssr-blog) ![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg) 4 | 5 | ![bg](https://src.sweetalkos.com/DmobetUUUAAhlD_.jpg) 6 | A server-side-rending blog based on Vue2 && Koa2 && MongoDB with Persona5 theme. 7 | 8 | ## Preview 9 | 10 | [click here](https://www.sweetalkos.com) 11 | 12 | 13 | 14 | 15 | ## Usage in Development 16 | 17 | ### What You Need Before Getting Started 18 | 19 | * Node.js 20 | * MongoDB 21 | * Redis 22 | 23 | ### server 24 | 25 | ```node 26 | cd server 27 | npm install 28 | npm start // default serve at localhost: 3000 29 | ``` 30 | 31 | You can change config options at `server/conf/config.js` 32 | 33 | ### admin 34 | 35 | ```node 36 | cd admin 37 | npm install 38 | npm run dev // default serve at localhost: 8082 39 | ``` 40 | 41 | ### front 42 | 43 | ```node 44 | cd front 45 | npm install 46 | npm run dev // default serve at localhost: 8080 47 | ``` 48 | 49 | ## Usage in Production 50 | 51 | ### What You Need Before Getting Started 52 | 53 | * Docker 54 | 55 | You should change the image to your own in `docker-compose.yml` 56 | 57 | ```node 58 | ... 59 | web_front: 60 | image: unsad/web_front // change to your own 61 | ... 62 | web_back: 63 | image: unsad/web_back // change to your own 64 | ... 65 | ``` 66 | 67 | ```node 68 | docker-compose up // default server at localhost:4000 69 | ``` 70 | ## Lisence 71 | 72 | [MIT](https://opensource.org/licenses/MIT) 73 | 74 | -------------------------------------------------------------------------------- /admin/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /admin/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /admin/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /admin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | 'rules': { 20 | // allow paren-less arrow functions 21 | 'arrow-parens': 0, 22 | // allow async-await 23 | 'generator-star-spacing': 0, 24 | "semi": 0, 25 | 'space-before-function-paren': 0, 26 | 'no-unused-vars': 0, 27 | // allow debugger during development 28 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /admin/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | test/unit/coverage 8 | test/e2e/reports 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /admin/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /admin/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:7.7 3 | 4 | # copy all files to target 5 | COPY . /app 6 | 7 | # install dependences 8 | RUN cd /app/admin && npm install && npm run build 9 | 10 | # clean cache 11 | RUN npm cache clean 12 | 13 | WORKDIR /app 14 | 15 | EXPOSE 8082 -------------------------------------------------------------------------------- /admin/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, function (err, stats) { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /admin/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | function exec (cmd) { 7 | return require('child_process').execSync(cmd).toString().trim() 8 | } 9 | 10 | const versionRequirements = [ 11 | { 12 | name: 'node', 13 | currentVersion: semver.clean(process.version), 14 | versionRequirement: packageConfig.engines.node 15 | } 16 | ] 17 | 18 | if (shell.which('npm')) { 19 | versionRequirements.push({ 20 | name: 'npm', 21 | currentVersion: exec('npm --version'), 22 | versionRequirement: packageConfig.engines.npm 23 | }) 24 | } 25 | 26 | module.exports = function () { 27 | const warnings = [] 28 | for (let i = 0; i < versionRequirements.length; i++) { 29 | const mod = versionRequirements[i] 30 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 31 | warnings.push(mod.name + ': ' + 32 | chalk.red(mod.currentVersion) + ' should be ' + 33 | chalk.green(mod.versionRequirement) 34 | ) 35 | } 36 | } 37 | 38 | if (warnings.length) { 39 | console.log('') 40 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 41 | console.log() 42 | for (let i = 0; i < warnings.length; i++) { 43 | const warning = warnings[i] 44 | console.log(' ' + warning) 45 | } 46 | console.log() 47 | process.exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /admin/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict' 3 | require('eventsource-polyfill') 4 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 5 | 6 | hotClient.subscribe(function (event) { 7 | if (event.action === 'reload') { 8 | window.location.reload() 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /admin/build/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | const config = require('../config') 5 | if (!process.env.NODE_ENV) { 6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 7 | } 8 | 9 | const opn = require('opn') 10 | const path = require('path') 11 | const express = require('express') 12 | const webpack = require('webpack') 13 | const proxyMiddleware = require('http-proxy-middleware') 14 | const webpackConfig = (process.env.NODE_ENV === 'testing' || process.env.NODE_ENV === 'production') 15 | ? require('./webpack.prod.conf') 16 | : require('./webpack.dev.conf') 17 | 18 | // default port where dev server listens for incoming traffic 19 | const port = process.env.PORT || config.dev.port 20 | // automatically open browser, if not set will be false 21 | const autoOpenBrowser = !!config.dev.autoOpenBrowser 22 | // Define HTTP proxies to your custom API backend 23 | // https://github.com/chimurai/http-proxy-middleware 24 | const proxyTable = config.dev.proxyTable 25 | 26 | const app = express() 27 | const compiler = webpack(webpackConfig) 28 | 29 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 30 | publicPath: webpackConfig.output.publicPath, 31 | quiet: true 32 | }) 33 | 34 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 35 | log: false, 36 | heartbeat: 2000 37 | }) 38 | // force page reload when html-webpack-plugin template changes 39 | // currently disabled until this is resolved: 40 | // https://github.com/jantimon/html-webpack-plugin/issues/680 41 | // compiler.plugin('compilation', function (compilation) { 42 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 43 | // hotMiddleware.publish({ action: 'reload' }) 44 | // cb() 45 | // }) 46 | // }) 47 | 48 | // enable hot-reload and state-preserving 49 | // compilation error display 50 | app.use(hotMiddleware) 51 | 52 | // proxy api requests 53 | Object.keys(proxyTable).forEach(function (context) { 54 | let options = proxyTable[context] 55 | if (typeof options === 'string') { 56 | options = { target: options } 57 | } 58 | app.use(proxyMiddleware(options.filter || context, options)) 59 | }) 60 | 61 | // handle fallback for HTML5 history API 62 | app.use(require('connect-history-api-fallback')()) 63 | 64 | // serve webpack bundle output 65 | app.use(devMiddleware) 66 | 67 | // serve pure static assets 68 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 69 | app.use(staticPath, express.static('./static')) 70 | 71 | const uri = 'http://localhost:' + port 72 | 73 | var _resolve 74 | var _reject 75 | var readyPromise = new Promise((resolve, reject) => { 76 | _resolve = resolve 77 | _reject = reject 78 | }) 79 | 80 | var server 81 | var portfinder = require('portfinder') 82 | portfinder.basePort = port 83 | 84 | console.log('> Starting dev server...') 85 | devMiddleware.waitUntilValid(() => { 86 | portfinder.getPort((err, port) => { 87 | if (err) { 88 | _reject(err) 89 | } 90 | process.env.PORT = port 91 | var uri = 'http://localhost:' + port 92 | console.log('> Listening at ' + uri + '\n') 93 | // when env is testing, don't need open it 94 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 95 | opn(uri) 96 | } 97 | server = app.listen(port) 98 | _resolve() 99 | }) 100 | }) 101 | 102 | module.exports = { 103 | ready: readyPromise, 104 | close: () => { 105 | server.close() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /admin/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | exports.assetsPath = function (_path) { 7 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 8 | ? config.build.assetsSubDirectory 9 | : config.dev.assetsSubDirectory 10 | return path.posix.join(assetsSubDirectory, _path) 11 | } 12 | 13 | exports.cssLoaders = function (options) { 14 | options = options || {} 15 | 16 | const cssLoader = { 17 | loader: 'css-loader', 18 | options: { 19 | minimize: process.env.NODE_ENV === 'production', 20 | sourceMap: options.sourceMap 21 | } 22 | } 23 | 24 | // generate loader string to be used with extract text plugin 25 | function generateLoaders (loader, loaderOptions) { 26 | const loaders = [cssLoader] 27 | if (loader) { 28 | loaders.push({ 29 | loader: loader + '-loader', 30 | options: Object.assign({}, loaderOptions, { 31 | sourceMap: options.sourceMap 32 | }) 33 | }) 34 | } 35 | 36 | // Extract CSS when that option is specified 37 | // (which is the case during production build) 38 | if (options.extract) { 39 | return ExtractTextPlugin.extract({ 40 | use: loaders, 41 | fallback: 'vue-style-loader' 42 | }) 43 | } else { 44 | return ['vue-style-loader'].concat(loaders) 45 | } 46 | } 47 | 48 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 49 | return { 50 | css: generateLoaders(), 51 | postcss: generateLoaders(), 52 | less: generateLoaders('less'), 53 | sass: generateLoaders('sass', { indentedSyntax: true }), 54 | scss: generateLoaders('sass'), 55 | stylus: generateLoaders('stylus'), 56 | styl: generateLoaders('stylus') 57 | } 58 | } 59 | 60 | // Generate loaders for standalone style files (outside of .vue) 61 | exports.styleLoaders = function (options) { 62 | const output = [] 63 | const loaders = exports.cssLoaders(options) 64 | for (const extension in loaders) { 65 | const loader = loaders[extension] 66 | output.push({ 67 | test: new RegExp('\\.' + extension + '$'), 68 | use: loader 69 | }) 70 | } 71 | return output 72 | } 73 | -------------------------------------------------------------------------------- /admin/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | 6 | module.exports = { 7 | loaders: utils.cssLoaders({ 8 | sourceMap: isProduction 9 | ? config.build.productionSourceMap 10 | : config.dev.cssSourceMap, 11 | extract: isProduction 12 | }), 13 | transformToRequire: { 14 | video: 'src', 15 | source: 'src', 16 | img: 'src', 17 | image: 'xlink:href' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /admin/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | module.exports = { 12 | entry: { 13 | app: './src/main.js' 14 | }, 15 | output: { 16 | path: config.build.assetsRoot, 17 | filename: '[name].js', 18 | publicPath: process.env.NODE_ENV === 'production' 19 | ? config.build.assetsPublicPath 20 | : config.dev.assetsPublicPath 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.vue', '.json'], 24 | alias: { 25 | 'vue$': 'vue/dist/vue.esm.js', 26 | '@': resolve('src'), 27 | } 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.(js|vue)$/, 33 | loader: 'eslint-loader', 34 | enforce: 'pre', 35 | include: [resolve('src'), resolve('test')], 36 | options: { 37 | formatter: require('eslint-friendly-formatter') 38 | } 39 | }, 40 | { 41 | test: /\.vue$/, 42 | loader: 'vue-loader', 43 | options: vueLoaderConfig 44 | }, 45 | { 46 | test: /\.js$/, 47 | loader: 'babel-loader', 48 | include: [resolve('src'), resolve('test')] 49 | }, 50 | { 51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 52 | loader: 'url-loader', 53 | options: { 54 | limit: 10000, 55 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 56 | } 57 | }, 58 | { 59 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 60 | loader: 'url-loader', 61 | options: { 62 | limit: 10000, 63 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 64 | } 65 | }, 66 | { 67 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 68 | loader: 'url-loader', 69 | options: { 70 | limit: 10000, 71 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 72 | } 73 | } 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /admin/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const baseWebpackConfig = require('./webpack.base.conf') 7 | const HtmlWebpackPlugin = require('html-webpack-plugin') 8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 9 | 10 | // add hot-reload related code to entry chunks 11 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 12 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 13 | }) 14 | 15 | module.exports = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 18 | }, 19 | // cheap-module-eval-source-map is faster for development 20 | devtool: '#cheap-module-eval-source-map', 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': config.dev.env 24 | }), 25 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.NoEmitOnErrorsPlugin(), 28 | // https://github.com/ampedandwired/html-webpack-plugin 29 | new HtmlWebpackPlugin({ 30 | filename: 'index.html', 31 | template: 'index.html', 32 | inject: true 33 | }), 34 | new FriendlyErrorsPlugin() 35 | ] 36 | }) 37 | -------------------------------------------------------------------------------- /admin/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | 13 | const env = process.env.NODE_ENV === 'testing' 14 | ? require('../config/test.env') 15 | : config.build.env 16 | 17 | const webpackConfig = merge(baseWebpackConfig, { 18 | module: { 19 | rules: utils.styleLoaders({ 20 | sourceMap: config.build.productionSourceMap, 21 | extract: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? '#source-map' : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | // UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify 36 | new webpack.optimize.UglifyJsPlugin({ 37 | compress: { 38 | warnings: false 39 | }, 40 | sourceMap: true 41 | }), 42 | // extract css into its own file 43 | new ExtractTextPlugin({ 44 | filename: utils.assetsPath('css/[name].[contenthash].css') 45 | }), 46 | // Compress extracted CSS. We are using this plugin so that possible 47 | // duplicated CSS from different components can be deduped. 48 | new OptimizeCSSPlugin({ 49 | cssProcessorOptions: { 50 | safe: true 51 | } 52 | }), 53 | // generate dist index.html with correct asset hash for caching. 54 | // you can customize output by editing /index.html 55 | // see https://github.com/ampedandwired/html-webpack-plugin 56 | new HtmlWebpackPlugin({ 57 | filename: process.env.NODE_ENV === 'testing' 58 | ? 'index.html' 59 | : config.build.index, 60 | template: 'index.html', 61 | inject: true, 62 | minify: { 63 | removeComments: true, 64 | collapseWhitespace: true, 65 | removeAttributeQuotes: true 66 | // more options: 67 | // https://github.com/kangax/html-minifier#options-quick-reference 68 | }, 69 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 70 | chunksSortMode: 'dependency' 71 | }), 72 | // keep module.id stable when vender modules does not change 73 | new webpack.HashedModuleIdsPlugin(), 74 | // split vendor js into its own file 75 | new webpack.optimize.CommonsChunkPlugin({ 76 | name: 'vendor', 77 | minChunks: function (module) { 78 | // any required modules inside node_modules are extracted to vendor 79 | return ( 80 | module.resource && 81 | /\.js$/.test(module.resource) && 82 | module.resource.indexOf( 83 | path.join(__dirname, '../node_modules') 84 | ) === 0 85 | ) 86 | } 87 | }), 88 | // extract webpack runtime and module manifest to its own file in order to 89 | // prevent vendor hash from being updated whenever app bundle is updated 90 | new webpack.optimize.CommonsChunkPlugin({ 91 | name: 'manifest', 92 | chunks: ['vendor'] 93 | }), 94 | // copy custom static assets 95 | new CopyWebpackPlugin([ 96 | { 97 | from: path.resolve(__dirname, '../static'), 98 | to: config.build.assetsSubDirectory, 99 | ignore: ['.*'] 100 | } 101 | ]) 102 | ] 103 | }) 104 | 105 | if (config.build.productionGzip) { 106 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 107 | 108 | webpackConfig.plugins.push( 109 | new CompressionWebpackPlugin({ 110 | asset: '[path].gz[query]', 111 | algorithm: 'gzip', 112 | test: new RegExp( 113 | '\\.(' + 114 | config.build.productionGzipExtensions.join('|') + 115 | ')$' 116 | ), 117 | threshold: 10240, 118 | minRatio: 0.8 119 | }) 120 | ) 121 | } 122 | 123 | if (config.build.bundleAnalyzerReport) { 124 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 125 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 126 | } 127 | 128 | module.exports = webpackConfig 129 | -------------------------------------------------------------------------------- /admin/build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // This is the webpack config used for unit tests. 3 | 4 | const utils = require('./utils') 5 | const webpack = require('webpack') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | 9 | const webpackConfig = merge(baseWebpackConfig, { 10 | // use inline sourcemap for karma-sourcemap-loader 11 | module: { 12 | rules: utils.styleLoaders() 13 | }, 14 | devtool: '#inline-source-map', 15 | resolveLoader: { 16 | alias: { 17 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option 18 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 19 | 'scss-loader': 'sass-loader' 20 | } 21 | }, 22 | plugins: [ 23 | new webpack.DefinePlugin({ 24 | 'process.env': require('../config/test.env') 25 | }) 26 | ] 27 | }) 28 | 29 | // no need for app entry during tests 30 | delete webpackConfig.entry 31 | 32 | module.exports = webpackConfig 33 | -------------------------------------------------------------------------------- /admin/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /admin/config/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | // Template version: 1.1.1 4 | // see http://vuejs-templates.github.io/webpack for documentation. 5 | 6 | const path = require('path') 7 | 8 | module.exports = { 9 | build: { 10 | env: require('./prod.env'), 11 | index: path.resolve(__dirname, '../dist/index.html'), 12 | assetsRoot: path.resolve(__dirname, '../dist'), 13 | assetsSubDirectory: 'static', 14 | assetsPublicPath: '/', 15 | productionSourceMap: true, 16 | // Gzip off by default as many popular static hosts such as 17 | // Surge or Netlify already gzip all static assets for you. 18 | // Before setting to `true`, make sure to: 19 | // npm install --save-dev compression-webpack-plugin 20 | productionGzip: false, 21 | productionGzipExtensions: ['js', 'css'], 22 | // Run the build command with an extra argument to 23 | // View the bundle analyzer report after build finishes: 24 | // `npm run build --report` 25 | // Set to `true` or `false` to always turn it on or off 26 | bundleAnalyzerReport: process.env.npm_config_report 27 | }, 28 | dev: { 29 | env: require('./dev.env'), 30 | port: process.env.PORT || 8082, 31 | autoOpenBrowser: true, 32 | assetsSubDirectory: 'static', 33 | assetsPublicPath: '/', 34 | proxyTable: { 35 | '/proxyPrefix': { 36 | target: 'http://localhost:3000/', 37 | changeOrigin: true, 38 | pathRewrite: { 39 | '^/proxyPrefix': '' 40 | } 41 | } 42 | }, 43 | // CSS Sourcemaps off by default because relative paths are "buggy" 44 | // with this option, according to the CSS-Loader README 45 | // (https://github.com/webpack/css-loader#sourcemaps) 46 | // In our experience, they generally work as expected, 47 | // just be aware of this issue when enabling this option. 48 | cssSourceMap: false 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /admin/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /admin/config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | element 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "element", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "unsad ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js", 11 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 12 | "e2e": "node test/e2e/runner.js", 13 | "test": "npm run unit && npm run e2e", 14 | "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" 15 | }, 16 | "dependencies": { 17 | "axios": "^0.19.0", 18 | "classnames": "^2.2.5", 19 | "element-ui": "^1.4.6", 20 | "highlight.js": "^9.12.0", 21 | "lodash": "^4.0.0", 22 | "marked": "^0.3.6", 23 | "marked-toc": "^0.3.0", 24 | "moment": "^2.18.1", 25 | "portfinder": "^1.0.13", 26 | "stylus": "^0.54.5", 27 | "stylus-loader": "^3.0.1", 28 | "vue": "^2.3.3", 29 | "vuex": "^3.0.0", 30 | "vuex-router-sync": "^3.0.0", 31 | "vue-router": "^2.3.1" 32 | }, 33 | "devDependencies": { 34 | "autoprefixer": "^6.7.2", 35 | "babel-core": "^6.22.1", 36 | "babel-eslint": "^7.1.1", 37 | "babel-loader": "^6.2.10", 38 | "babel-plugin-istanbul": "^4.1.1", 39 | "babel-plugin-transform-runtime": "^6.22.0", 40 | "babel-preset-env": "^1.3.2", 41 | "babel-preset-stage-2": "^6.22.0", 42 | "babel-register": "^6.22.0", 43 | "chai": "^3.5.0", 44 | "chalk": "^1.1.3", 45 | "chromedriver": "^2.27.2", 46 | "connect-history-api-fallback": "^1.3.0", 47 | "copy-webpack-plugin": "^4.0.1", 48 | "cross-env": "^4.0.0", 49 | "cross-spawn": "^5.0.1", 50 | "css-loader": "^0.28.0", 51 | "eslint": "^4.19.1", 52 | "eslint-config-standard": "^6.2.1", 53 | "eslint-friendly-formatter": "^2.0.7", 54 | "eslint-loader": "^1.7.1", 55 | "eslint-plugin-html": "^2.0.0", 56 | "eslint-plugin-promise": "^3.4.0", 57 | "eslint-plugin-standard": "^2.0.1", 58 | "eventsource-polyfill": "^0.9.6", 59 | "express": "^4.14.1", 60 | "extract-text-webpack-plugin": "^2.0.0", 61 | "file-loader": "^0.11.1", 62 | "friendly-errors-webpack-plugin": "^1.1.3", 63 | "html-webpack-plugin": "^2.28.0", 64 | "http-proxy-middleware": "^0.17.3", 65 | "inject-loader": "^3.0.0", 66 | "karma": "^1.4.1", 67 | "karma-coverage": "^1.1.1", 68 | "karma-mocha": "^1.3.0", 69 | "karma-phantomjs-launcher": "^1.0.2", 70 | "karma-phantomjs-shim": "^1.4.0", 71 | "karma-sinon-chai": "^1.3.1", 72 | "karma-sourcemap-loader": "^0.3.7", 73 | "karma-spec-reporter": "0.0.30", 74 | "karma-webpack": "^2.0.2", 75 | "lolex": "^1.5.2", 76 | "mocha": "^3.2.0", 77 | "nightwatch": "^0.9.12", 78 | "opn": "^4.0.2", 79 | "optimize-css-assets-webpack-plugin": "^1.3.0", 80 | "ora": "^1.2.0", 81 | "phantomjs-prebuilt": "^2.1.14", 82 | "rimraf": "^2.6.0", 83 | "selenium-server": "^3.0.1", 84 | "semver": "^5.3.0", 85 | "shelljs": "^0.7.6", 86 | "sinon": "^2.1.0", 87 | "sinon-chai": "^2.8.0", 88 | "url-loader": "^0.5.8", 89 | "vue-loader": "^12.1.0", 90 | "vue-style-loader": "^3.0.1", 91 | "vue-template-compiler": "^2.3.3", 92 | "webpack": "^2.6.1", 93 | "webpack-bundle-analyzer": "^3.5.2", 94 | "webpack-dev-middleware": "^1.10.0", 95 | "webpack-hot-middleware": "^2.18.0", 96 | "webpack-merge": "^4.1.0" 97 | }, 98 | "engines": { 99 | "node": ">= 4.0.0", 100 | "npm": ">= 3.0.0" 101 | }, 102 | "browserslist": [ 103 | "> 1%", 104 | "last 2 versions", 105 | "not ie <= 8" 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 54 | 55 | 60 | -------------------------------------------------------------------------------- /admin/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unsad/ssr-blog/8e5a4d84657f8b3b71362a46ed7322bd5e189863/admin/src/assets/logo.png -------------------------------------------------------------------------------- /admin/src/components/Main.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 40 | -------------------------------------------------------------------------------- /admin/src/components/containers/Create.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 137 | 140 | -------------------------------------------------------------------------------- /admin/src/components/containers/List.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 81 | 84 | -------------------------------------------------------------------------------- /admin/src/components/pages/AlbumCreate.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 86 | 89 | -------------------------------------------------------------------------------- /admin/src/components/pages/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 78 | 81 | -------------------------------------------------------------------------------- /admin/src/components/pages/Login.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 74 | 77 | -------------------------------------------------------------------------------- /admin/src/components/pages/Logout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | 42 | -------------------------------------------------------------------------------- /admin/src/components/pages/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 84 | 90 | -------------------------------------------------------------------------------- /admin/src/components/pages/Top.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 59 | 62 | -------------------------------------------------------------------------------- /admin/src/components/utils/marked.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by unsad on 2017/10/18. 3 | */ 4 | import Marked from 'marked' 5 | import hljs from 'highlight.js' 6 | 7 | const renderer = new Marked.Renderer(); 8 | export const toc = []; 9 | 10 | renderer.heading = function (text, level) { 11 | let slug = text.toLowerCase().replace(/[:~/+\s]/g, '-'); 12 | toc.push({ 13 | level, 14 | slug, 15 | title: text 16 | }); 17 | return `${text}` 18 | } 19 | 20 | Marked.setOptions({ 21 | highlight: function(code, lang) { 22 | if (hljs.getLanguage(lang)) { 23 | return hljs.highlight(lang, code).value; 24 | } else { 25 | return hljs.highlightAuto(code).value; 26 | } 27 | }, 28 | renderer 29 | }); 30 | 31 | export const marked = text => { 32 | let tok = Marked.lexer(text); 33 | text = Marked.parser(tok).replace(/
/ig, '
')
34 |   return text;
35 | };
36 | 
37 | 


--------------------------------------------------------------------------------
/admin/src/components/views/CreateEditView.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/10/14.
 3 |  */
 4 | import Item from '../containers/Create.vue'
 5 | 
 6 | export default function(options) {
 7 |   return {
 8 |     name: `${options.name}-create-view`,
 9 |     asyncData({store}) {
10 |       return store.dispatch('FETCH_LIST', options);
11 |     },
12 |     render(h) {
13 |       return h(Item, {props: {options}});
14 |     }
15 |   }
16 | }
17 | 


--------------------------------------------------------------------------------
/admin/src/components/views/CreateListView.js:
--------------------------------------------------------------------------------
 1 | import Item from '../containers/List.vue';
 2 | 
 3 | export default function (options) {
 4 |   return {
 5 |     name: `${options.name}-list-view`,
 6 |     asyncData({store}) {
 7 |       return store.dispatch('FETCH_LIST', options);
 8 |     },
 9 |     render(h) {
10 |       return h(Item, {props: {options: options}});
11 |     }
12 |   }
13 | }
14 | 


--------------------------------------------------------------------------------
/admin/src/components/views/CreateMarkdownView.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/10/15.
 3 |  */
 4 | import Item from '../containers/Post.vue'
 5 | 
 6 | export default function (options) {
 7 |   return {
 8 |     name: `${options.name}-markdown-view`,
 9 |     asyncData({store}) {
10 |       return store.dispatch('FETCH_LIST', options);
11 |     },
12 |     render(h) {
13 |       return h(Item, {props: {options}});
14 |     }
15 |   }
16 | }
17 | 


--------------------------------------------------------------------------------
/admin/src/main.js:
--------------------------------------------------------------------------------
 1 | // The Vue build version to load with the `import` command
 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
 3 | import Vue from 'vue'
 4 | import App from './App'
 5 | import router from './router'
 6 | import store from './store/index'
 7 | import { sync } from 'vuex-router-sync'
 8 | 
 9 | sync(store, router);
10 | 
11 | import 'element-ui/lib/theme-default/index.css'
12 | import Element from 'element-ui'
13 | 
14 | Vue.use(Element);
15 | 
16 | Vue.config.productionTip = false
17 | 
18 | /* eslint-disable no-new */
19 | new Vue({
20 |   el: '#app',
21 |   router,
22 |   store,
23 |   template: '',
24 |   components: { App }
25 | })
26 | 


--------------------------------------------------------------------------------
/admin/src/store/api.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/5/23.
 3 |  */
 4 | import axios from 'axios';
 5 | 
 6 | const root = '/proxyPrefix/api';
 7 | 
 8 | const store = {};
 9 | 
10 | export default store;
11 | 
12 | function isObject(obj) {
13 |   return Object.prototype.toString.call(obj).slice(8, -1) === 'Object';
14 | }
15 | 
16 | function convertObjectToArray(args) {
17 |   return isObject(args) ? Object.keys(args).map((value, index) => {
18 |     let temp = {};
19 |     temp[value] = args[value];
20 |     return temp;
21 |   }) : [];
22 | }
23 | store.axios = axios;
24 | 
25 | store.login = (conditions, args) => {
26 |   return axios.post(`/proxyPrefix/admin/login`, conditions);
27 | };
28 | 
29 | store.logout = (conditions, args) => {
30 |   return axios.post(`/proxyPrefix/admin/logout`, conditions);
31 | };
32 | 
33 | store.getImageHeight = url => {
34 |   return axios.get(`${url}?imageInfo`).then(response => response.data);
35 | };
36 | 
37 | store.getImageToken = body => {
38 |   return axios.post(`/proxyPrefix/admin/qiniu`, body).then(response => response.data);
39 | };
40 | 
41 | store.fetchList = (model, query) => {
42 |   let target = `${root}/${model}`;
43 |   return axios.get(target, {params: query}).then(response => response.data);
44 | };
45 | 
46 | store.fetchByID = (model, id, query) => {
47 |   let target = `${root}/${model}/${id}`;
48 |   return axios.get(target, {params: query}).then(response => response.data);
49 | };
50 | 
51 | store.patchByID = (model, id, form) => {
52 |   let target = `${root}/${model}/${id}`;
53 |   return axios.patch(target, form).then(response => response.data);
54 | };
55 | 
56 | store.post = (model, form) => {
57 |   let target = `${root}/${model}`;
58 |   return axios.post(target, form).then(response => response.data);
59 | };
60 | 
61 | store.deleteByID = (model, id) => {
62 |   let target = `${root}/${model}/${id}`;
63 |   return axios.delete(target).then(response => response.data);
64 | };
65 | 
66 | 


--------------------------------------------------------------------------------
/admin/src/store/index.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Created by unsad on 2017/10/14.
  3 |  */
  4 | import Vue from 'vue'
  5 | import Vuex from 'vuex'
  6 | import api from './api'
  7 | 
  8 | Vue.use(Vuex);
  9 | 
 10 | const store = new Vuex.Store({
 11 |   state: {
 12 |     siteInfo: {},
 13 |     list: [],
 14 |     curr: {},
 15 |     user: {}
 16 |   },
 17 |   actions: {
 18 |     GET_IMAGE_HEIGHT: ({commit, state}, {url}) => {
 19 |       return api.getImageHeight(url).then(data => data.height || 100);
 20 |     },
 21 |     FETCH: ({commit, state}, {model, query}) => {
 22 |       return api.fetchList(model, query);
 23 |     },
 24 |     FETCH_BY_ID: ({commit, state}, {model, id, query}) => {
 25 |       return api.fetchByID(model, id, query);
 26 |     },
 27 |     FETCH_LIST: ({commit, state}, {model, query}) => {
 28 |       return api.fetchList(model, query).then(obj => {
 29 |         commit('SET_LIST', {obj})
 30 |       })
 31 |     },
 32 |     FETCH_CREATE: ({commit, state}, {model, id, query}) => {
 33 |       if (id === -1) {
 34 |         return api.fetchList(model, query).then(curr => {
 35 |           let obj = curr.reduce((prev, value) => {
 36 |             if (model === 'option') {
 37 |               prev[value.key] = value.value
 38 |             } else if (model === 'user') {
 39 |               Object.keys(value).forEach(item => {
 40 |                 prev[item] = value[item]
 41 |               })
 42 |             }
 43 |             return prev
 44 |           }, {});
 45 |           commit('SET_CURR', {obj})
 46 |         })
 47 |       } else {
 48 |         return api.fetchByID(model, id, query).then(obj => {
 49 |           console.log('fetch id', obj);
 50 |           commit('SET_CURR', {obj})
 51 |         })
 52 |       }
 53 |     },
 54 | 
 55 |     POST: ({commit, state}, {model, form}) => {
 56 |       if (model !== 'post' && model !== 'option' && model !== 'user') {
 57 |         return api.post(model, form)
 58 |       } else if (model === 'user' || model === 'post') {
 59 |         let {_id: id} = form;
 60 |         if (typeof id !== 'undefined') {
 61 |           return api.patchByID(model, id, form).then(result => {
 62 |             if (model === 'user') {
 63 |               commit('SET_USER', {user: result});
 64 |             }
 65 |             return result;
 66 |           });
 67 |         } else {
 68 |           return api.post(model, form);
 69 |         }
 70 |       } else if (model === 'option') {
 71 |         let promiseArr = Object.keys(form).reduce((prev, curr) => {
 72 |           if (form[curr] !== state.curr[curr]) {
 73 |             const {_id: id} = state.siteInfo[curr];
 74 |             let promise = api.patchByID(model, id, {
 75 |               value: form[curr]
 76 |             });
 77 |             prev.push(promise)
 78 |           }
 79 |           return prev
 80 |         }, []);
 81 |         return Promise.all(promiseArr)
 82 |       }
 83 |     },
 84 |     GET_IMAGE_TOKEN: ({commit, state}, body) => {
 85 |       return api.getImageToken(body);
 86 |     },
 87 |     PATCH: ({commit, state}, {model, id, form}) => {
 88 |       return api.patchByID(model, id, form)
 89 |     },
 90 |     FETCH_USER: ({commit, state}, {model, query, username}) => {
 91 |       return api.fetchList(model, query).then(result => {
 92 |         for (let i = 0, len = result.length; i < len; i++) {
 93 |           let user = result[i];
 94 |           if (user.name === username) {
 95 |             commit('SET_USER', {user});
 96 |             break;
 97 |           }
 98 |         }
 99 |       });
100 |     },
101 |     DELETE: ({commit, state}, {model, id}) => {
102 |       console.log(model, id);
103 |       return api.deleteByID(model, id)
104 |     },
105 |     FETCH_OPTIONS: ({commit, state}) => {
106 |       return api.fetchList('option', {}).then(optionArr => {
107 |         let obj = optionArr.reduce((prev, curr) => {
108 |           prev[curr.key] = curr;
109 |           return prev;
110 |         }, {});
111 |         commit('SET_OPTIONS', {obj});
112 |       })
113 |     }
114 |   },
115 | 
116 |   mutations: {
117 |     SET_LIST: (state, {obj}) => {
118 |       Vue.set(state, 'list', obj);
119 |     },
120 |     SET_OPTIONS: (state, {obj}) => {
121 |       Vue.set(state, 'siteInfo', obj);
122 |     },
123 |     SET_CURR: (state, {obj}) => {
124 |       Vue.set(state, 'curr', obj);
125 |     },
126 |     SET_USER: (state, { user }) => {
127 |       Vue.set(state, 'user', user);
128 |     }
129 |   },
130 |   getters: {}
131 | });
132 | 
133 | export default store;
134 | 


--------------------------------------------------------------------------------
/admin/src/utils/error.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/10/15.
 3 |  */
 4 | export const getChineseDesc = desc => {
 5 |   switch (desc) {
 6 |     case 'Token not found':
 7 |       return '请求失败,请确认您已登录';
 8 |     case 'Get token failed.Check the password':
 9 |       return '登录失败,请检查您的密码';
10 |     case 'Get token failed.Check the name':
11 |       return '登录失败,请检查您的账号';
12 |     case 'Token verify failed':
13 |     case 'Token invalid':
14 |       return 'Token验证失败,请重新登录';
15 |     default:
16 |       return desc;
17 |   }
18 | };
19 | 


--------------------------------------------------------------------------------
/admin/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unsad/ssr-blog/8e5a4d84657f8b3b71362a46ed7322bd5e189863/admin/static/.gitkeep


--------------------------------------------------------------------------------
/admin/test/e2e/custom-assertions/elementCount.js:
--------------------------------------------------------------------------------
 1 | // A custom Nightwatch assertion.
 2 | // the name of the method is the filename.
 3 | // can be used in tests like this:
 4 | //
 5 | //   browser.assert.elementCount(selector, count)
 6 | //
 7 | // for how to write custom assertions see
 8 | // http://nightwatchjs.org/guide#writing-custom-assertions
 9 | exports.assertion = function (selector, count) {
10 |   this.message = 'Testing if element <' + selector + '> has count: ' + count
11 |   this.expected = count
12 |   this.pass = function (val) {
13 |     return val === this.expected
14 |   }
15 |   this.value = function (res) {
16 |     return res.value
17 |   }
18 |   this.command = function (cb) {
19 |     var self = this
20 |     return this.api.execute(function (selector) {
21 |       return document.querySelectorAll(selector).length
22 |     }, [selector], function (res) {
23 |       cb.call(self, res)
24 |     })
25 |   }
26 | }
27 | 


--------------------------------------------------------------------------------
/admin/test/e2e/nightwatch.conf.js:
--------------------------------------------------------------------------------
 1 | require('babel-register')
 2 | var config = require('../../config')
 3 | 
 4 | // http://nightwatchjs.org/gettingstarted#settings-file
 5 | module.exports = {
 6 |   src_folders: ['test/e2e/specs'],
 7 |   output_folder: 'test/e2e/reports',
 8 |   custom_assertions_path: ['test/e2e/custom-assertions'],
 9 | 
10 |   selenium: {
11 |     start_process: true,
12 |     server_path: require('selenium-server').path,
13 |     host: '127.0.0.1',
14 |     port: 4444,
15 |     cli_args: {
16 |       'webdriver.chrome.driver': require('chromedriver').path
17 |     }
18 |   },
19 | 
20 |   test_settings: {
21 |     default: {
22 |       selenium_port: 4444,
23 |       selenium_host: 'localhost',
24 |       silent: true,
25 |       globals: {
26 |         devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
27 |       }
28 |     },
29 | 
30 |     chrome: {
31 |       desiredCapabilities: {
32 |         browserName: 'chrome',
33 |         javascriptEnabled: true,
34 |         acceptSslCerts: true
35 |       }
36 |     },
37 | 
38 |     firefox: {
39 |       desiredCapabilities: {
40 |         browserName: 'firefox',
41 |         javascriptEnabled: true,
42 |         acceptSslCerts: true
43 |       }
44 |     }
45 |   }
46 | }
47 | 


--------------------------------------------------------------------------------
/admin/test/e2e/runner.js:
--------------------------------------------------------------------------------
 1 | // 1. start the dev server using production config
 2 | process.env.NODE_ENV = 'testing'
 3 | var server = require('../../build/dev-server.js')
 4 | 
 5 | server.ready.then(() => {
 6 |   // 2. run the nightwatch test suite against it
 7 |   // to run in additional browsers:
 8 |   //    1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
 9 |   //    2. add it to the --env flag below
10 |   // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
11 |   // For more information on Nightwatch's config file, see
12 |   // http://nightwatchjs.org/guide#settings-file
13 |   var opts = process.argv.slice(2)
14 |   if (opts.indexOf('--config') === -1) {
15 |     opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
16 |   }
17 |   if (opts.indexOf('--env') === -1) {
18 |     opts = opts.concat(['--env', 'chrome'])
19 |   }
20 | 
21 |   var spawn = require('cross-spawn')
22 |   var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })
23 | 
24 |   runner.on('exit', function (code) {
25 |     server.close()
26 |     process.exit(code)
27 |   })
28 | 
29 |   runner.on('error', function (err) {
30 |     server.close()
31 |     throw err
32 |   })
33 | })
34 | 


--------------------------------------------------------------------------------
/admin/test/e2e/specs/test.js:
--------------------------------------------------------------------------------
 1 | // For authoring Nightwatch tests, see
 2 | // http://nightwatchjs.org/guide#usage
 3 | 
 4 | module.exports = {
 5 |   'default e2e tests': function (browser) {
 6 |     // automatically uses dev Server port from /config.index.js
 7 |     // default: http://localhost:8080
 8 |     // see nightwatch.conf.js
 9 |     const devServer = browser.globals.devServerURL
10 | 
11 |     browser
12 |       .url(devServer)
13 |       .waitForElementVisible('#app', 5000)
14 |       .assert.elementPresent('.hello')
15 |       .assert.containsText('h1', 'Welcome to Your Vue.js App')
16 |       .assert.elementCount('img', 1)
17 |       .end()
18 |   }
19 | }
20 | 


--------------------------------------------------------------------------------
/admin/test/unit/.eslintrc:
--------------------------------------------------------------------------------
 1 | {
 2 |   "env": {
 3 |     "mocha": true
 4 |   },
 5 |   "globals": {
 6 |     "expect": true,
 7 |     "sinon": true
 8 |   }
 9 | }
10 | 


--------------------------------------------------------------------------------
/admin/test/unit/index.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue'
 2 | 
 3 | Vue.config.productionTip = false
 4 | 
 5 | // require all test files (files that ends with .spec.js)
 6 | const testsContext = require.context('./specs', true, /\.spec$/)
 7 | testsContext.keys().forEach(testsContext)
 8 | 
 9 | // require all src files except main.js for coverage.
10 | // you can also change this to match only the subset of files that
11 | // you want coverage for.
12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
13 | srcContext.keys().forEach(srcContext)
14 | 


--------------------------------------------------------------------------------
/admin/test/unit/karma.conf.js:
--------------------------------------------------------------------------------
 1 | // This is a karma config file. For more details see
 2 | //   http://karma-runner.github.io/0.13/config/configuration-file.html
 3 | // we are also using it with karma-webpack
 4 | //   https://github.com/webpack/karma-webpack
 5 | 
 6 | var webpackConfig = require('../../build/webpack.test.conf')
 7 | 
 8 | module.exports = function (config) {
 9 |   config.set({
10 |     // to run in additional browsers:
11 |     // 1. install corresponding karma launcher
12 |     //    http://karma-runner.github.io/0.13/config/browsers.html
13 |     // 2. add it to the `browsers` array below.
14 |     browsers: ['PhantomJS'],
15 |     frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
16 |     reporters: ['spec', 'coverage'],
17 |     files: ['./index.js'],
18 |     preprocessors: {
19 |       './index.js': ['webpack', 'sourcemap']
20 |     },
21 |     webpack: webpackConfig,
22 |     webpackMiddleware: {
23 |       noInfo: true
24 |     },
25 |     coverageReporter: {
26 |       dir: './coverage',
27 |       reporters: [
28 |         { type: 'lcov', subdir: '.' },
29 |         { type: 'text-summary' }
30 |       ]
31 |     }
32 |   })
33 | }
34 | 


--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
 1 | version: '3'
 2 | services:
 3 |   nginx_proxy:
 4 |     image: nginx:1.15.2-alpine
 5 |     restart: always
 6 |     volumes:
 7 |       - ./nginx.conf:/etc/nginx/nginx.conf
 8 |     ports:
 9 |       - 4000:80
10 |   mongo_db:
11 |     image: mongo:3.4
12 |     restart: always
13 |     volumes:
14 |       - ~/data/db:/data/db
15 |     command: ["mongod", "--port", "19999"]
16 |     expose: 
17 |       - "19999"
18 |   redis:
19 |     image: redis:3.2
20 |     restart: always
21 |     expose:
22 |       - "6379"
23 |   web_front:
24 |     image: unsad/web_front
25 |     build:
26 |       context: ./front
27 |       dockerfile: ./Dockerfile
28 |     depends_on:
29 |       - web_back 
30 |     expose:
31 |       - "8080"
32 |   web_back:
33 |     image: unsad/web_back
34 |     build:
35 |       context: ./server
36 |       dockerfile: ./Dockerfile
37 |     depends_on:
38 |       - mongo_db
39 |       - redis
40 |     expose:
41 |       - "3000"
42 | 
43 |   


--------------------------------------------------------------------------------
/front/.babelrc:
--------------------------------------------------------------------------------
 1 | {
 2 |   "presets": [
 3 |     ["env", { "modules": false }],
 4 |     "stage-2"
 5 |   ],
 6 |   "plugins": ["transform-runtime"],
 7 |   "comments": false,
 8 |   "env": {
 9 |     "test": {
10 |       "presets": ["env", "stage-2"],
11 |       "plugins": [ "istanbul" ]
12 |     }
13 |   }
14 | }
15 | 


--------------------------------------------------------------------------------
/front/.editorconfig:
--------------------------------------------------------------------------------
 1 | root = true
 2 | 
 3 | [*]
 4 | charset = utf-8
 5 | indent_style = space
 6 | indent_size = 2
 7 | end_of_line = lf
 8 | insert_final_newline = true
 9 | trim_trailing_whitespace = true
10 | 


--------------------------------------------------------------------------------
/front/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 | 


--------------------------------------------------------------------------------
/front/.eslintrc.js:
--------------------------------------------------------------------------------
 1 | // http://eslint.org/docs/user-guide/configuring
 2 | 
 3 | module.exports = {
 4 |   root: true,
 5 |   env: {
 6 |     node: true
 7 |   },
 8 |   extends: ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"],
 9 |   // add your custom rules here
10 |   rules: {
11 |     'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
12 |     "prettier/prettier": [
13 |       "warn",
14 |       {
15 |         "singleQuote": true,
16 |         "semi": true,
17 |         "trailingComma": "none"
18 |       }
19 |     ]
20 |   },
21 |   parserOptions: {
22 |     parser: "@typescript-eslint/parser"
23 |   }
24 | }
25 | 


--------------------------------------------------------------------------------
/front/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 | test/unit/coverage
7 | test/e2e/reports
8 | selenium-debug.log
9 | /dist


--------------------------------------------------------------------------------
/front/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 | 
3 | module.exports = {
4 |   "plugins": {
5 |     // to edit target browsers: use "browserlist" field in ff.json
6 |     "autoprefixer": {}
7 |   }
8 | }
9 | 


--------------------------------------------------------------------------------
/front/Dockerfile:
--------------------------------------------------------------------------------
 1 | 
 2 | FROM node:11.6
 3 | 
 4 | # copy all files to target
 5 | COPY . /app
 6 | RUN npm set registry https://registry.npm.taobao.org
 7 | RUN npm i -g pm2
 8 | # clean cache
 9 | RUN npm cache verify
10 | 
11 | WORKDIR /app
12 | EXPOSE 8080
13 | 
14 | 
15 | CMD ["pm2-docker", "start", "pm2.json", "--env", "production"] 


--------------------------------------------------------------------------------
/front/build/setup-dev-server.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/9/24.
 3 |  */
 4 | const path = require('path');
 5 | const webpack = require('webpack');
 6 | const MFS = require('memory-fs');
 7 | const proxyTable = require('../config/index').dev.proxyTable;
 8 | const clientConfig = require('./webpack.client.config');
 9 | const serverConfig = require('./webpack.server.config');
10 | const proxyMiddleware = require('http-proxy-middleware');
11 | 
12 | const readFile = (fs, file) => {
13 |   try {
14 |     return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8');
15 |   } catch (e) {
16 | 
17 |   }
18 | };
19 | 
20 | module.exports = function setupDevServer(app, cb) {
21 |   let bundle, clientManifest;
22 |   let resolve;
23 |   const readyPromise = new Promise(r => {
24 |     resolve = r;
25 |   });
26 |   const ready = (...args) => {
27 |     resolve();
28 |     cb(...args);
29 |   };
30 | 
31 | // modify client config to work with hot middleware
32 |   clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app];
33 |   clientConfig.output.filename = '[name].js';
34 |   clientConfig.plugins.push(
35 |     new webpack.HotModuleReplacementPlugin(),
36 |     new webpack.NoEmitOnErrorsPlugin()
37 |   );
38 | 
39 | // dev middleware
40 |   const clientCompiler = webpack(clientConfig);
41 |   const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
42 |     publicPath: clientConfig.output.publicPath,
43 |     noInfo: true
44 |   });
45 |   app.use(devMiddleware);
46 |   clientCompiler.plugin('done', stats => {
47 |     stats = stats.toJson();
48 |     stats.errors.forEach(err => console.error(err));
49 |     stats.warnings.forEach(err => console.warn(err));
50 |     if (stats.errors.length) return;
51 | 
52 |     clientManifest = JSON.parse(readFile(
53 |       devMiddleware.fileSystem,
54 |       'vue-ssr-client-manifest.json'
55 |     ));
56 |     if (bundle) {
57 |       ready(bundle, {
58 |         clientManifest
59 |       })
60 |     }
61 |   });
62 | 
63 |   // hot middleware
64 |   app.use(require('webpack-hot-middleware')(clientCompiler, {heartbeat: 5000}));
65 | 
66 |   // proxy middleware
67 |   Object.keys(proxyTable).forEach(function (context) {
68 |     let options = proxyTable[context];
69 |     if (typeof options === 'string') {
70 |       options = {target: options}
71 |     }
72 |     app.use(proxyMiddleware(context, options))
73 |   });
74 | 
75 |   // watch and update server renderer
76 |   const serverCompiler = webpack(serverConfig);
77 |   const mfs = new MFS();
78 |   serverCompiler.outputFileSystem = mfs;
79 |   serverCompiler.watch({}, (err, stats) => {
80 |     if (err) throw err;
81 |     stats = stats.toJson();
82 |     if (stats.errors.length) return;
83 | 
84 |     // read bundle generated by vue-ssr-webpack-plugin
85 |     bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'));
86 |     if (clientManifest) {
87 |       ready(bundle, {
88 |         clientManifest
89 |       })
90 |     }
91 |   });
92 | 
93 |   return readyPromise;
94 | };
95 | 
96 | 


--------------------------------------------------------------------------------
/front/build/vue-loader.config.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/9/21.
 3 |  */
 4 | module.exports = {
 5 |   preserveWhitespace: false,
 6 |   esModule: true,
 7 |   transformAssetUrls: {
 8 |     video: ['src', 'poster'],
 9 |     source: 'src',
10 |     img: 'src',
11 |     image: 'xlink:href',
12 |     object: 'data'
13 |   }
14 | };
15 | 


--------------------------------------------------------------------------------
/front/build/webpack.base.config.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Created by unsad on 2017/9/21.
  3 |  */
  4 | const path = require('path');
  5 | const webpack = require('webpack');
  6 | const vueConfig = require('./vue-loader.config');
  7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
  9 | const VueLoaderPlugin = require('vue-loader/lib/plugin');
 10 | 
 11 | const isProd = process.env.NODE_ENV === 'production';
 12 | const isServer = process.env.VUE_ENV === 'server';
 13 | 
 14 | function generateCssRules(cssLoaderOption) {
 15 |   return [
 16 |     'vue-style-loader',
 17 |     cssLoaderOption,
 18 |     postcssLoader,
 19 |     'stylus-loader'
 20 |   ];
 21 | }
 22 | 
 23 | const cssLoaderWithModule = {
 24 |   loader: 'css-loader',
 25 |   options: {
 26 |     url: true,
 27 |     modules: {
 28 |       localIdentName: '[local]_[hash:base64:8]'
 29 |     }
 30 |   }
 31 | };
 32 | 
 33 | const postcssLoader = {
 34 |   loader: 'postcss-loader',
 35 |   options: {
 36 |     ident: 'postcss',
 37 |     plugins: [
 38 |       require('postcss-cssnext')({
 39 |         warnForDuplicates: false
 40 |       }),
 41 |       require('cssnano')()
 42 |     ]
 43 |   }
 44 | };
 45 | 
 46 | function resolve(dir) {
 47 |   return path.join(__dirname, '..', dir);
 48 | }
 49 | 
 50 | module.exports = {
 51 |   mode: isProd ? 'production' : 'development',
 52 |   devtool: isProd ? false : 'cheap-module-source-map',
 53 |   output: {
 54 |     path: path.resolve(__dirname, '../dist'),
 55 |     publicPath: '/dist/',
 56 |     filename: '[name].[chunkhash].js'
 57 |   },
 58 |   context: path.resolve(__dirname, '..'),
 59 |   resolve: {
 60 |     extensions: ['.ts', '.js', '.vue', '.json'],
 61 |     alias: {
 62 |       'public': path.resolve(__dirname, '../public'),
 63 |       '@': resolve('src')
 64 |     }
 65 |   },
 66 |   module: {
 67 |     noParse: /es-promise\.js$/,
 68 |     rules: [
 69 |       // {
 70 |       //   enforce: 'pre',
 71 |       //   test: /\.(ts|js|vue)$/,
 72 |       //   loader: 'eslint-loader',
 73 |       //   exclude: /node_modules/,
 74 |       //   options: {
 75 |       //     formatter: require('eslint-friendly-formatter')
 76 |       //   }
 77 |       // },
 78 |       {
 79 |         test: /\.vue$/,
 80 |         loader: 'vue-loader',
 81 |         options: vueConfig
 82 |       },
 83 |       {
 84 |         test: /\.tsx?$/,
 85 |         loader: 'ts-loader',
 86 |         exclude: /node_modules/,
 87 |         options: {
 88 |           appendTsSuffixTo: [/\.vue$/],
 89 |           transpileOnly: true
 90 |         }
 91 |       },
 92 |       {
 93 |         test: /\.(styl(us)?|css)$/,
 94 |         oneOf: [
 95 |           {
 96 |             resourceQuery: /module/,
 97 |             use: generateCssRules(cssLoaderWithModule)
 98 |           },
 99 |           {
100 |             use: generateCssRules('css-loader')
101 |           }
102 |         ]
103 |       },
104 |       {
105 |         test: /\.(png|jpg|gif|svg|woff|woff2|ttf)$/,
106 |         loader: 'url-loader',
107 |         options: {
108 |           limit: 10000,
109 |           name: '[name].[hash].[ext]'
110 |         }
111 |       }
112 |     ]
113 |   },
114 |   performance: {
115 |     maxEntrypointSize: 300000,
116 |     hints: isProd ? 'warning' : false
117 |   },
118 |   plugins: (isProd ? [] : [
119 |     new FriendlyErrorsPlugin()
120 |   ]).concat([
121 |     new webpack.LoaderOptionsPlugin({
122 |       options: {
123 |         stylus: {
124 |           import: [path.resolve(__dirname, '../src/assets/css/variable.styl')]
125 |         }
126 |       }
127 |     }),
128 |     new VueLoaderPlugin()
129 |   ])
130 | };
131 | 


--------------------------------------------------------------------------------
/front/build/webpack.client.config.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Created by unsad on 2017/9/21.
  3 |  */
  4 | const glob = require('glob');
  5 | const path = require('path');
  6 | const webpack = require('webpack');
  7 | const merge = require('webpack-merge');
  8 | const base = require('./webpack.base.config');
  9 | const SWPrecachePlugin = require('sw-precache-webpack-plugin');
 10 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
 11 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 12 | 
 13 | const config = merge(base, {
 14 |   entry: {
 15 |     app: './src/client-entry.ts'
 16 |   },
 17 |   resolve: {
 18 |     alias: {
 19 |       'create-api': './create-api-client.js'
 20 |     }
 21 |   },
 22 |   optimization: {
 23 |     runtimeChunk: {
 24 |       name: 'manifest'
 25 |     },
 26 |     splitChunks: {
 27 |       cacheGroups: {
 28 |         commons: {
 29 |           minChunks: 2,
 30 |           name: 'commons',
 31 |           chunks: 'async',
 32 |           reuseExistingChunk: true,
 33 |           priority: 3,
 34 |         },
 35 |         vendors: {
 36 |           priority: 1,
 37 |           name: 'vendors',
 38 |           chunks: 'initial',
 39 |           test: /[\\/]node_modules[\\/]/
 40 |         }
 41 |       }
 42 |     }
 43 |   },
 44 |   plugins: [
 45 |     new webpack.DefinePlugin({
 46 |       'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
 47 |       'process.env.VUE_ENV': '"client"'
 48 |     }),
 49 |     new webpack.NamedModulesPlugin(),
 50 |     new VueSSRClientPlugin()
 51 |   ]
 52 | });
 53 | 
 54 | if (process.env.NODE_ENV === 'production') {
 55 |   config.plugins = config.plugins.concat([
 56 |     // auto generate service worker
 57 |     new SWPrecachePlugin({
 58 |       cacheId: 'blog',
 59 |       filename: 'service-worker.js',
 60 |       minify: true,
 61 |       dontCacheBustUrlsMatching: false,
 62 |       staticFileGlobsIgnorePatterns: [
 63 |         /\.json$/,
 64 |         /index\.html$/,
 65 |         /\.map$/,
 66 |         /\.css$/,
 67 |         /\.eot$/],
 68 |       mergeStaticsConfig: true,
 69 |       staticFileGlobs: [
 70 |         path.join(__dirname, '../dist/static/*.*')
 71 |       ],
 72 |       stripPrefixMulti: {
 73 |         [path.join(__dirname, '../dist/static')]: '/static'
 74 |       },
 75 |       runtimeCaching: [
 76 |         {
 77 |           urlPattern: '/',
 78 |           handler: 'networkFirst'
 79 |         },
 80 |         {
 81 |           urlPattern: /service-worker.js/,
 82 |           handler: 'networkOnly'
 83 |         },
 84 |         {
 85 |           // note that this pattern will cache ajax request
 86 |           urlPattern: /(.+\/[^\.]*$)/,
 87 |           handler: 'networkFirst',
 88 |           options: {
 89 |             cache: {
 90 |               maxEntries: 30,
 91 |               name: 'blog-runtime-cache'
 92 |             }
 93 |           }
 94 |         },
 95 |         { 
 96 |           urlPattern: /\.(png|jpg|webp|gif)/,
 97 |           handler: 'cacheFirst',
 98 |           options: {
 99 |             cache: {
100 |               maxEntries: 20,
101 |               name: 'blog-picture-cache'
102 |             }
103 |           }
104 |         }
105 |       ]
106 |     })
107 |   ],
108 |   new MiniCssExtractPlugin({
109 |     filename: 'common.[name].[hash].css',
110 |     chunkFilename: '[id].[hash].css'
111 |   }));
112 | }
113 | 
114 | module.exports = config;
115 | 
116 | 


--------------------------------------------------------------------------------
/front/build/webpack.server.config.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/9/21.
 3 |  */
 4 | const webpack = require('webpack');
 5 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 6 | const merge = require('webpack-merge');
 7 | const nodeExtenals = require('webpack-node-externals');
 8 | const baseConfig = require('./webpack.base.config.js');
 9 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
10 | 
11 | module.exports = merge(baseConfig, {
12 |   entry: './src/server-entry.ts',
13 |   target: 'node',
14 |   devtool: 'source-map',
15 |   output: {
16 |     libraryTarget: 'commonjs2',
17 |     filename: 'server-bundle.js'
18 |   },
19 |   resolve: {
20 |     alias: {
21 |       'create-api': './create-api-server.js'
22 |     }
23 |   },
24 |   externals: nodeExtenals({
25 |     whitelist: [/\.css$/, /\?vue&type=style/]
26 |   }),
27 |   plugins: [
28 |     new VueSSRServerPlugin(),
29 |     new webpack.DefinePlugin({
30 |       'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
31 |       'process.env.VUE_ENV': '"server"',
32 |       'process.env.serverHost': JSON.stringify(process.env.serverHost || 'localhost')
33 |     }),
34 |     new webpack.LoaderOptionsPlugin({
35 |       minimize: true
36 |     })
37 |   ]
38 | });
39 | 


--------------------------------------------------------------------------------
/front/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 | 
4 | module.exports = merge(prodEnv, {
5 |   NODE_ENV: '"development"'
6 | })
7 | 


--------------------------------------------------------------------------------
/front/config/index.js:
--------------------------------------------------------------------------------
 1 | // see http://vuejs-templates.github.io/webpack for documentation.
 2 | var path = require('path')
 3 | 
 4 | module.exports = {
 5 |   build: {
 6 |     env: require('./prod.env'),
 7 |     index: path.resolve(__dirname, '../dist/index.html'),
 8 |     assetsRoot: path.resolve(__dirname, '../dist'),
 9 |     assetsSubDirectory: 'static',
10 |     assetsPublicPath: '/',
11 |     productionSourceMap: true,
12 |     // Gzip off by default as many popular static hosts such as
13 |     // Surge or Netlify already gzip all static assets for you.
14 |     // Before setting to `true`, make sure to:
15 |     // npm install --save-dev compression-webpack-plugin
16 |     productionGzip: false,
17 |     productionGzipExtensions: ['js', 'css'],
18 |     // Run the build command with an extra argument to
19 |     // View the bundle analyzer report after build finishes:
20 |     // `npm run build --report`
21 |     // Set to `true` or `false` to always turn it on or off
22 |     bundleAnalyzerReport: process.env.npm_config_report
23 |   },
24 |   dev: {
25 |     env: require('./dev.env'),
26 |     overlay: true,
27 |     port: 8080,
28 |     autoOpenBrowser: true,
29 |     assetsSubDirectory: 'static',
30 |     assetsPublicPath: '/',
31 |     proxyTable: {
32 |       '/proxyPrefix': {
33 |         target: 'http://localhost:3000',
34 |         changeOrigin: true,
35 |         pathRewrite: {
36 |           '^/proxyPrefix': ''
37 |         }
38 |       }
39 |     },
40 |     // CSS Sourcemaps off by default because relative paths are "buggy"
41 |     // with this option, according to the CSS-Loader README
42 |     // (https://github.com/webpack/css-loader#sourcemaps)
43 |     // In our experience, they generally work as expected,
44 |     // just be aware of this issue when enabling this option.
45 |     cssSourceMap: false
46 |   }
47 | }
48 | 


--------------------------------------------------------------------------------
/front/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   NODE_ENV: '"production"'
3 | }
4 | 


--------------------------------------------------------------------------------
/front/config/test.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var devEnv = require('./dev.env')
3 | 
4 | module.exports = merge(devEnv, {
5 |   NODE_ENV: '"testing"'
6 | })
7 | 


--------------------------------------------------------------------------------
/front/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     ssr-blog
 6 |   
 7 |   
 8 |     
 9 |   
10 | 
11 | 


--------------------------------------------------------------------------------
/front/middleware/favicon.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/17.
 3 |  */
 4 | const fs = require('fs');
 5 | const serveFavicon = require('serve-favicon');
 6 | const log = require('log4js').getLogger('favicon');
 7 | const path = require('path');
 8 | 
 9 | module.exports = function (faviconPath, options) {
10 |   let _middleware
11 |   return function(req, res, next) {
12 |     if (_middleware) return _middleware(req, res, next);
13 |     const target = path.join(__dirname, `../${faviconPath}`);
14 |     fs.readFile(target, function(err, buf) {
15 |       if (err) {
16 |         log.error(err);
17 |         return res.status(404).end();
18 |       }
19 |       _middleware = serveFavicon(buf, options);
20 |       return _middleware(req, res, next);
21 |     })
22 |   }
23 | };
24 | 


--------------------------------------------------------------------------------
/front/middleware/serverGoogleAnalytic.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/5.
 3 |  */
 4 | const log = require('log4js').getLogger('google analytic')
 5 | 
 6 | const config = require('../server/config')
 7 | const request = require('axios')
 8 | const EMPTY_GIF = Buffer.from('R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAICTAEAOw==', 'base64')
 9 | const uuid = require('uuid')
10 | const expires = 3600 * 1000 * 24 * 365 * 2
11 | const ipReg = /\d+\.\d+\.\d+\.\d+/
12 | const lowerReg = /\s+/g
13 | 
14 | const shouldBanSpider = ua => {
15 |   if (!ua) {
16 |     return true
17 |   }
18 | 
19 |   ua = ua.toLowerCase().replace(lowerReg, '')
20 |   return config.ga.spider.some(item => ua.indexOf(item) > -1)
21 | }
22 | 
23 | const getClientIp = (req) => {
24 |   let matched = req.ip.match(ipReg)
25 |   return matched ? matched[0] : req.ip
26 | }
27 | 
28 | module.exports = (req, res, next, query) => {
29 |   let realQuery;
30 |   let clientId;
31 |   if (!query) {
32 |     clientId = req.cookies.id;
33 |     if (!clientId) {
34 |       clientId = uuid.v4();
35 |       res.cookie('id', clientId, {
36 |         expires: new Date(Date.now() + expires)
37 |       });
38 |     }
39 |     res.setHeader('Content-Type', 'image/gif');
40 |     res.setHeader('Content-Length', 43);
41 |     res.end(EMPTY_GIF);
42 |     realQuery = req.query;
43 |   } else {
44 |     realQuery = query;
45 |     clientId = realQuery.cid;
46 |   }
47 |   let passParams = config.ga.required.reduce((prev, curr) => {
48 |     if (!realQuery[curr]) {
49 |       prev = false
50 |     }
51 |     return prev
52 |   }, true)
53 | 
54 |   if (passParams === false) return
55 |   if (config.googleTrackID === '') return
56 |   let userAgent = req.header('user-agent')
57 |   if (shouldBanSpider(userAgent) === true) return
58 | 
59 |   let {z: timeStamp = Date.now() } = realQuery
60 |   let form = Object.assign({}, realQuery, {
61 |     v: config.ga.version,
62 |     tid: config.googleTrackID,
63 |     ds: 'web',
64 |     z: timeStamp + clientId,
65 |     cid: clientId,
66 |     uip: getClientIp(req),
67 |     ua: userAgent,
68 |     t: 'pageview',
69 |     an: config.title,
70 |     dh: config.siteUrl
71 |   })
72 |   request.get(config.ga.api, {
73 |     params: form
74 |   }).then(response => {
75 |     if (response.status !== 200) {
76 |       log.error(response, form)
77 |       return
78 |     }
79 |     log.info('Google Analytic sended', form.cid, form.ua)
80 |   }).catch(err => log.error(err, form))
81 | }
82 | 


--------------------------------------------------------------------------------
/front/package.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "name": "ssr-blog",
  3 |   "version": "1.0.0",
  4 |   "description": "A Vue.js project",
  5 |   "author": "unsad ",
  6 |   "private": true,
  7 |   "scripts": {
  8 |     "dev": "node server",
  9 |     "build": "rimraf dist && npm run build:client && npm run build:server",
 10 |     "build:client": "cross-env NODE_ENV=production  VUE_ENV=client webpack --config build/webpack.client.config.js --progress --hide-modules",
 11 |     "build:server": "cross-env NODE_ENV=production serverHost=web_back  VUE_ENV=server webpack --config build/webpack.server.config.js --progress --hide-modules",
 12 |     "start": "cross-env NODE_ENV=production node server"
 13 |   },
 14 |   "dependencies": {
 15 |     "@types/node": "^12.7.11",
 16 |     "@vue/eslint-config-prettier": "^5.0.0",
 17 |     "@vue/eslint-config-typescript": "^4.0.0",
 18 |     "axios": "^0.19.0",
 19 |     "body-parser": "^1.18.2",
 20 |     "compression": "^1.7.0",
 21 |     "glob": "^7.1.2",
 22 |     "hls.js": "^0.12.4",
 23 |     "jsdom": "^15.1.1",
 24 |     "memory-fs": "^0.4.1",
 25 |     "node-schedule": "^1.2.5",
 26 |     "normalize.css": "^8.0.1",
 27 |     "route-cache": "^0.4.3",
 28 |     "serve-favicon": "^2.4.4",
 29 |     "stylus": "^0.54.5",
 30 |     "stylus-loader": "^3.0.1",
 31 |     "sw-precache-webpack-plugin": "^0.11.5",
 32 |     "vue": "^2.5.17",
 33 |     "vue-class-component": "^7.1.0",
 34 |     "vue-meta": "^2.3.0",
 35 |     "vue-property-decorator": "^8.2.2",
 36 |     "vue-router": "^3.0.1",
 37 |     "vuex": "^3.0.1",
 38 |     "vuex-class": "^0.3.1",
 39 |     "vuex-router-sync": "^5.0.0",
 40 |     "webpack-node-externals": "^1.6.0"
 41 |   },
 42 |   "devDependencies": {
 43 |     "@typescript-eslint/eslint-plugin": "^2.3.2",
 44 |     "@typescript-eslint/parser": "^2.3.2",
 45 |     "autoprefixer": "^9.6.4",
 46 |     "babel-core": "^6.22.1",
 47 |     "babel-eslint": "^10.0.3",
 48 |     "babel-loader": "^7.1.4",
 49 |     "babel-plugin-istanbul": "^5.2.0",
 50 |     "babel-plugin-transform-runtime": "^6.22.0",
 51 |     "babel-preset-env": "^1.3.2",
 52 |     "babel-preset-stage-2": "^6.22.0",
 53 |     "babel-register": "^6.22.0",
 54 |     "chai": "^4.2.0",
 55 |     "chalk": "^2.4.2",
 56 |     "chromedriver": "^77.0.0",
 57 |     "connect-history-api-fallback": "^1.3.0",
 58 |     "cookie-parser": "^1.4.3",
 59 |     "copy-webpack-plugin": "^5.0.4",
 60 |     "cross-env": "^6.0.3",
 61 |     "cross-spawn": "^7.0.0",
 62 |     "css-loader": "^3.2.0",
 63 |     "cssnano": "^4.0.2",
 64 |     "es6-promise": "^4.1.1",
 65 |     "eslint": "^6.5.1",
 66 |     "eslint-config-standard": "^14.1.0",
 67 |     "eslint-friendly-formatter": "^4.0.1",
 68 |     "eslint-loader": "^2.1.0",
 69 |     "eslint-plugin-html": "^6.0.0",
 70 |     "eslint-plugin-import": "^2.13.0",
 71 |     "eslint-plugin-node": "^10.0.0",
 72 |     "eslint-plugin-prettier": "^3.1.1",
 73 |     "eslint-plugin-promise": "^4.2.1",
 74 |     "eslint-plugin-standard": "^4.0.1",
 75 |     "eslint-plugin-vue": "^5.2.3",
 76 |     "eventsource-polyfill": "^0.9.6",
 77 |     "express": "^4.14.1",
 78 |     "extract-text-webpack-plugin": "^3.0.2",
 79 |     "file-loader": "^4.2.0",
 80 |     "friendly-errors-webpack-plugin": "^1.1.3",
 81 |     "html-webpack-plugin": "^3.0.6",
 82 |     "http-proxy-middleware": "^0.20.0",
 83 |     "inject-loader": "^4.0.1",
 84 |     "karma": "^4.3.0",
 85 |     "karma-coverage": "^2.0.1",
 86 |     "karma-mocha": "^1.3.0",
 87 |     "karma-phantomjs-launcher": "^1.0.2",
 88 |     "karma-phantomjs-shim": "^1.4.0",
 89 |     "karma-sinon-chai": "^2.0.2",
 90 |     "karma-sourcemap-loader": "^0.3.7",
 91 |     "karma-spec-reporter": "0.0.32",
 92 |     "karma-webpack": "^4.0.2",
 93 |     "log4js": "^5.2.0",
 94 |     "lolex": "^4.2.0",
 95 |     "lru-cache": "^5.1.1",
 96 |     "mini-css-extract-plugin": "^0.8.0",
 97 |     "mocha": "^6.2.1",
 98 |     "nightwatch": "^1.2.4",
 99 |     "opn": "^6.0.0",
100 |     "optimize-css-assets-webpack-plugin": "^5.0.3",
101 |     "ora": "^4.0.2",
102 |     "phantomjs-prebuilt": "^2.1.14",
103 |     "postcss-cssnext": "^3.1.0",
104 |     "postcss-loader": "^3.0.0",
105 |     "prettier": "^1.18.2",
106 |     "rimraf": "^3.0.0",
107 |     "selenium-server": "^3.0.1",
108 |     "semver": "^6.3.0",
109 |     "serialize-javascript": "^2.1.0",
110 |     "shelljs": "^0.8.3",
111 |     "sinon": "^7.5.0",
112 |     "sinon-chai": "^3.3.0",
113 |     "style-loader": "^1.0.0",
114 |     "sugarss": "^2.0.0",
115 |     "ts-loader": "^6.2.0",
116 |     "typescript": "^3.6.3",
117 |     "uglifyjs-webpack-plugin": "^2.2.0",
118 |     "url-loader": "^2.2.0",
119 |     "uuid": "^3.1.0",
120 |     "vue-eslint-parser": "^6.0.4",
121 |     "vue-loader": "^15.2.4",
122 |     "vue-server-renderer": "^2.5.17",
123 |     "vue-style-loader": "^4.0.2",
124 |     "vue-template-compiler": "^2.5.17",
125 |     "webpack": "^4.12.0",
126 |     "webpack-bundle-analyzer": "^3.5.2",
127 |     "webpack-cli": "^3.1.0",
128 |     "webpack-dev-middleware": "^3.7.2",
129 |     "webpack-hot-middleware": "^2.25.0",
130 |     "webpack-merge": "^4.1.0"
131 |   },
132 |   "engines": {
133 |     "node": ">= 4.0.0",
134 |     "npm": ">= 3.0.0"
135 |   },
136 |   "browserslist": [
137 |     "> 1%",
138 |     "last 2 versions",
139 |     "not ie <= 8"
140 |   ]
141 | }
142 | 


--------------------------------------------------------------------------------
/front/pm2.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "apps": [
 3 |     {
 4 |       "name": "frontend",
 5 |       "script": "server.js",
 6 |       "cwd": "",
 7 |       "exec_mode": "cluster",
 8 |       "instances": 0,
 9 |       "max_memory_restart": "256M",
10 |       "autorestart": true,
11 |       "node_args": [],
12 |       "args": [],
13 |       "env": {
14 |         "HOST": "localhost"
15 |       },
16 |       "env_production": {
17 |         "NODE_ENV": "production",
18 |         "serverHost": "web_back"
19 |       }
20 |     }
21 |   ]
22 | }


--------------------------------------------------------------------------------
/front/server/config.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Created by unsad on 2017/10/8.
  3 |  */
  4 | const isProd = process.env.NODE_ENV === 'production';
  5 | let axios = require('axios');
  6 | const { ssrPort, serverPort, serverHost, enableServerSideGA } = require('./mongo');
  7 | 
  8 | let siteUrl = 'http://localhost:8080';
  9 | let title = 'Blog';
 10 | let description = '';
 11 | let googleTrackID = '';
 12 | let favicon = isProd ? './dist' : '.';
 13 | let ga = {
 14 |   version: 1,
 15 |   api: 'https://www.google-analytics.com/collect',
 16 |   required: ['dt', 'dr', 'dp', 'z'],
 17 |   spider: [
 18 |     'spider',
 19 |     'bot',
 20 |     'appid',
 21 |     'go-http-client',
 22 |     'loadtest',
 23 |     'feed'
 24 |   ].map(item => item.toLowerCase().replace(/\s+/g, ''))
 25 | }
 26 | 
 27 | function flushOption() {
 28 |   return axios.get(`http://${serverHost}:${serverPort}/api/option`).then(res => {
 29 |     let options = res.data.reduce((prev, curr) => {
 30 |       prev[curr.key] = curr.value;
 31 |       return prev;
 32 |     }, {});
 33 |     siteUrl = options['siteUrl'];
 34 |     title = options['title'];
 35 |     description = options['description'];
 36 |     googleTrackID = options['analyzeCode'];
 37 |     favicon += options['faviconUrl'];
 38 |   });
 39 | }
 40 | 
 41 | exports.ssrPort = ssrPort;
 42 | exports.serverHost = serverHost;
 43 | exports.serverPort = serverPort;
 44 | exports.enableServerSideGA = enableServerSideGA;
 45 | 
 46 | Object.defineProperty(exports, 'favicon', {
 47 |   enumerable: true,
 48 |   get: function() {
 49 |     return favicon;
 50 |   },
 51 |   set: function(value) {
 52 |     favicon = value;
 53 |   }
 54 | });
 55 | 
 56 | Object.defineProperty(exports, 'title', {
 57 |   enumerable: true,
 58 |   get: function() {
 59 |     return title;
 60 |   },
 61 |   set: function(value) {
 62 |     title = value;
 63 |   }
 64 | });
 65 | 
 66 | Object.defineProperty(exports, 'siteUrl', {
 67 |   enumerable: true,
 68 |   get: function() {
 69 |     return siteUrl;
 70 |   },
 71 |   set: function(value) {
 72 |     siteUrl = value;
 73 |   }
 74 | });
 75 | 
 76 | Object.defineProperty(exports, 'googleTrackID', {
 77 |   enumerable: true,
 78 |   get: function() {
 79 |     return googleTrackID;
 80 |   },
 81 |   set: function(value) {
 82 |     googleTrackID = value;
 83 |   }
 84 | });
 85 | 
 86 | Object.defineProperty(exports, 'description', {
 87 |   enumerable: true,
 88 |   get: function() {
 89 |     return description;
 90 |   },
 91 |   set: function(value) {
 92 |     description = value;
 93 |   }
 94 | });
 95 | 
 96 | Object.defineProperty(exports, 'flushOption', {
 97 |   enumerable: true,
 98 |   get: function() {
 99 |     return flushOption;
100 |   }
101 | });
102 | 
103 | Object.defineProperty(exports, 'ga', {
104 |   enumerable: true,
105 |   get: function() {
106 |     return ga;
107 |   }
108 | });
109 | 


--------------------------------------------------------------------------------
/front/server/mongo.js:
--------------------------------------------------------------------------------
 1 | const env = process.env;
 2 | 
 3 | module.exports = {
 4 |   ssrPort: env.ssrPort || 8080,
 5 |   serverPort: env.serverPort || 3000,
 6 |   serverHost: env.serverHost || 'localhost',
 7 |   redisPassword: env.redisPassword || '',
 8 |   enableServerSideGA: env.enableServerSideGA || false
 9 | }
10 | 


--------------------------------------------------------------------------------
/front/server/robots.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/10/8.
 3 |  */
 4 | module.exports = config =>
 5 |   `User-agent: *
 6 |   
 7 |   Allow: /
 8 |   Sitemap: ${config.siteUrl}/sitemap.xml
 9 |   
10 |   User-agent: YisouSpider
11 |   Disallow: /
12 |   User-agent: EasouSpider
13 |   Disallow: /
14 |   User-agent: EtaoSpider
15 |   Disallow: /
16 |   User-agent: MJ12bot
17 |   Disallow: /`
18 | 


--------------------------------------------------------------------------------
/front/server/rss.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/10/8.
 3 |  */
 4 | let getUpdatedDate = date => `${date}\r\n`
 5 | 
 6 | const { serverHost, serverPort } = require('./config');
 7 | 
 8 | let tail = `  
 9 | `;
10 | 
11 | let api = `http://${serverHost}:${serverPort}/api/post`;
12 | 
13 | let params = {
14 |   conditions: {
15 |     type: 'post',
16 |     isPublic: true
17 |   },
18 |   select: {
19 |     pathName: 1,
20 |     createdAt: 1,
21 |     content: 1,
22 |     title: 1
23 |   },
24 |   sort: {
25 |     createdAt: -1
26 |   },
27 |   limit: 10
28 | };
29 | 
30 | let getRssBodyFromBody = (result, config) => {
31 |   let head = `
32 |   
33 |     ${config.title}
34 |     ${config.siteUrl}
35 |     ${config.description}
36 |     
37 |     zh-cn\r\n`
38 |   let body = result.data.reduce((prev, curr)=>{
39 |     let date = new Date(curr.updatedAt).toUTCString()
40 |     let content = curr.content.replace(/&/g, '&')
41 |       .replace(//g, '>')
43 |       .replace(/"/g, '"')
44 |       .replace(/'/g, ''')
45 |     prev += `    \r\n`;
46 |     prev += `      ${curr.title}\r\n`;
47 |     prev += `      ${config.siteUrl}/post/${curr.pathName}\r\n`;
48 |     prev += `      ${content}\r\n`;
49 |     prev += `      ${date}\r\n`;
50 |     prev += `      ${config.siteUrl}/post/${curr.pathName}\r\n`;
51 |     prev += `    \r\n`;
52 |     return prev;
53 |   }, '');
54 |   return head + getUpdatedDate(new Date().toUTCString()) + body + tail;
55 | }
56 | 
57 | module.exports = {
58 |   api,
59 |   params,
60 |   getRssBodyFromBody
61 | }
62 | 


--------------------------------------------------------------------------------
/front/server/sitemap.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/10/8.
 3 |  */
 4 | let head = `
 5 | \r\n`
 6 | const { serverHost, serverPort } = require('./config');
 7 | 
 8 | let tail = ``;
 9 | 
10 | let api = `http://${serverHost}:${serverPort}/api/post`;
11 | 
12 | let params = {
13 |   conditions: {
14 |     type: 'post',
15 |     isPublic: true
16 |   },
17 |   select: {
18 |     pathName: 1,
19 |     updatedAt: 1
20 |   },
21 |   sort: {
22 |     updatedAt: -1
23 |   }
24 | };
25 | 
26 | let getSitemapFromBody = (result, config) => {
27 |   let res = result.data;
28 |   let body = res.reduce((prev, curr)=>{
29 |     prev += `  \r\n`;
30 |     prev += `    ${config.siteUrl}/post/${curr.pathName}\r\n`;
31 |     prev += `    ${curr.updatedAt.slice(0, 10)}\r\n`;
32 |     prev += `    0.6\r\n`;
33 |     prev += `  \r\n`;
34 |     return prev;
35 |   }, '');
36 |   return head + body + tail;
37 | }
38 | 
39 | module.exports = {
40 |   api,
41 |   params,
42 |   getSitemapFromBody
43 | }
44 | 


--------------------------------------------------------------------------------
/front/src/App.vue:
--------------------------------------------------------------------------------
  1 | 
 14 | 
 15 | 
 73 | 
122 | 


--------------------------------------------------------------------------------
/front/src/assets/css/index.styl:
--------------------------------------------------------------------------------
 1 | @import './variable.styl'
 2 | 
 3 | @font-face 
 4 |   font-family: 'special-for-me'
 5 |   src: url('../fonts/ljh.ttf') format('truetype')
 6 |   font-weight: normal
 7 |   font-style: normal
 8 | 
 9 | @font-face 
10 |   font-family: 'jackey_handwriteregular';
11 |   src: url('../fonts/jackeyhandwrite-webfont.woff2') format('woff2'),
12 |        url('../fonts/jackeyhandwrite-webfont.woff') format('woff'),
13 |        url('../fonts/jackeyhandwrite-webfont.ttf') format('truetype');
14 |   font-weight: normal;
15 |   font-style: normal;
16 | 
17 | 
18 | *,
19 | *::before,
20 | *::after 
21 |   margin: 0;
22 |   padding: 0;
23 |   box-sizing: border-box;
24 | html 
25 |   font-size: $font-size-base
26 |   font-family: PingFang SC
27 |   background: #1e1e1e
28 | ul
29 |   list-style-type: none 
30 | a
31 |   text-decoration: none
32 | ::-webkit-scrollbar
33 |   background: transparent
34 |   width: 0
35 | 
36 | @media only screen and (min-width : 769px)
37 |   .no-pc
38 |     display: none!important
39 | 
40 | @media only screen and (max-width : 768px)
41 |   .no-phone
42 |     display: none!important


--------------------------------------------------------------------------------
/front/src/assets/css/mixin.styl:
--------------------------------------------------------------------------------
 1 | two-color-border(border = 1rem, outline = 0.2rem)
 2 |     color: #fff
 3 |     background: #000 
 4 |     border: border solid #fff
 5 |     outline: outline solid #000
 6 | 
 7 | title-base() 
 8 |     text-align: center
 9 |     color: #fff
10 |     line-height: 6rem
11 |     margin: 0 auto 1rem
12 |     font-family: 'special-for-me' 
13 |     background: url(../../../static/title_base.png)
14 | 
15 | no-wrap()
16 |     overflow: hidden
17 |     text-overflow:ellipsis
18 |     white-space: nowrap


--------------------------------------------------------------------------------
/front/src/assets/css/variable.styl:
--------------------------------------------------------------------------------
 1 | 
 2 | $font-color = #fff
 3 | $font-size-base = 12px
 4 | $font-size-small = 1rem
 5 | $font-size-small-u = 1.2rem
 6 | $font-size-middle-d = 1.5rem
 7 | $font-size-middle = 2rem
 8 | $font-size-large = 4rem
 9 | 
10 | $space-small = 0.5rem
11 | $space-middle = 1rem
12 | $space-middle-u = 1.5rem
13 | $space-large = 4rem
14 | 
15 | $main-color = rgb(230, 30, 30)
16 | $main-second-color = black
17 | $main-third-color = white
18 | 
19 | $content-bg-color = #1e1e1e
20 | $content-main-color = #c6c6c6
21 | $content-strong-color = #d9d9d9


--------------------------------------------------------------------------------
/front/src/assets/fonts/jackeyhandwrite-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unsad/ssr-blog/8e5a4d84657f8b3b71362a46ed7322bd5e189863/front/src/assets/fonts/jackeyhandwrite-webfont.ttf


--------------------------------------------------------------------------------
/front/src/assets/fonts/jackeyhandwrite-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unsad/ssr-blog/8e5a4d84657f8b3b71362a46ed7322bd5e189863/front/src/assets/fonts/jackeyhandwrite-webfont.woff


--------------------------------------------------------------------------------
/front/src/assets/fonts/jackeyhandwrite-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unsad/ssr-blog/8e5a4d84657f8b3b71362a46ed7322bd5e189863/front/src/assets/fonts/jackeyhandwrite-webfont.woff2


--------------------------------------------------------------------------------
/front/src/assets/fonts/ljh.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unsad/ssr-blog/8e5a4d84657f8b3b71362a46ed7322bd5e189863/front/src/assets/fonts/ljh.ttf


--------------------------------------------------------------------------------
/front/src/client-entry.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/9/20.
 3 |  */
 4 | import Vue from 'vue';
 5 | import { createApp } from './main';
 6 | import clientGoogleAnalyse from './utils/clientGoogleAnalyse';
 7 | import makeResponsive from './assets/js/base';
 8 | 
 9 | Vue.mixin({
10 |   beforeRouteUpdate(to, from, next) {
11 |     const { asyncData } = this.$options;
12 |     if (asyncData) {
13 |       asyncData({
14 |         store: this.$store,
15 |         route: to
16 |       }).then(next).catch(next);
17 |     } else {
18 |       next();
19 |     }
20 |   }
21 | });
22 | const { app, router, store, isProd, preFetchComponent } = createApp();
23 | 
24 | router.onReady(() => {
25 |   if (window.__INITIAL_STATE__) {
26 |     makeResponsive();
27 |     window.__INITIAL_STATE__.route.hash = window.location.hash;
28 |     store.replaceState(window.__INITIAL_STATE__);
29 |   }
30 | 
31 | // service worker
32 |   if (isProd && 'serviceWorker' in navigator && window.location.protocol === 'https:') {
33 |     navigator.serviceWorker.register('/service-worker.js');
34 |   }
35 | 
36 |   const beforeResolveHook = (to, from, next) => {
37 |     const matched = router.getMatchedComponents(to);
38 |     const prevMatched = router.getMatchedComponents(from);
39 |     let diffed = false;
40 |     const activated = matched.filter((c, i) => {
41 |       if (!diffed) {
42 |         diffed = prevMatched[i] !== c;
43 |       }
44 |       return diffed;
45 |     });
46 |     const asyncDataHooks = activated.map(c => ((c.options || {}).asyncData)).filter(_ => _);
47 | 
48 |     store.dispatch('SET_TRAN', to.name !== from.name);
49 | 
50 |     if (to.path === from.path && to.hash !== from.hash) {
51 |       return next();
52 |     }
53 | 
54 |     let loadingPromise = store.dispatch('START_LOADING');
55 |     let endLoadingCallback = (path?) => {
56 |       return loadingPromise.then(interval => {
57 |         clearInterval(interval);
58 |         setTimeout(() => {
59 |           store.dispatch('SET_PROGRESS', 100);
60 |           next(path);
61 |         }, 500);
62 |       });
63 |     };
64 | 
65 |     if (!asyncDataHooks.length) return endLoadingCallback();
66 | 
67 |     Promise.all(asyncDataHooks.map(hook => hook({ store, route: to }))).then(endLoadingCallback).catch(err => {
68 |       console.error(Date.now().toLocaleString(), err);
69 |       endLoadingCallback(false);
70 |     });
71 |   };
72 | 
73 |   router.beforeResolve(beforeResolveHook);
74 | 
75 |   if (window.__INITIAL_STATE__ && window.__INITIAL_STATE__.siteInfo) {
76 |     let analyzeCode = window.__INITIAL_STATE__.siteInfo.analyzeCode;
77 |     if (analyzeCode && analyzeCode.value !== '') {
78 |       router.afterEach((to, from) => {
79 |         from.name && setTimeout(() => {
80 |           if (to.path !== from.path) {
81 |             clientGoogleAnalyse(to.path || '/');
82 |           }
83 |         });
84 |       });
85 |     }
86 |   }
87 | 
88 |   if (typeof window.__INITIAL_STATE__ === 'undefined') {
89 |     beforeResolveHook(router.currentRoute, {}, () => {});
90 |     Promise.all(
91 |       preFetchComponent.map(component => component.asyncData(store, router.currentRoute))
92 |     ).then(() => makeResponsive());
93 |   }
94 |   app.$mount('#app');
95 | });
96 | 
97 | 


--------------------------------------------------------------------------------
/front/src/components/Back/index.vue:
--------------------------------------------------------------------------------
 1 | 
 6 | 
 7 | 
20 | 
38 | 


--------------------------------------------------------------------------------
/front/src/components/Disqus/index.vue:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 5 | 
 6 | 
52 | 
53 | 
54 | 


--------------------------------------------------------------------------------
/front/src/components/Footer/index.vue:
--------------------------------------------------------------------------------
 1 | 
14 | 
15 | 
24 | 
40 | 


--------------------------------------------------------------------------------
/front/src/components/Header/index.vue:
--------------------------------------------------------------------------------
 1 | 
11 | 
12 | 
20 | 
23 | 


--------------------------------------------------------------------------------
/front/src/components/Link/index.vue:
--------------------------------------------------------------------------------
  1 | 
 17 | 
 18 | 
 74 | 
134 | 


--------------------------------------------------------------------------------
/front/src/components/Loading/index.vue:
--------------------------------------------------------------------------------
 1 | 
 9 | 
10 | 
65 | 
94 | 


--------------------------------------------------------------------------------
/front/src/components/Pagination/index.vue:
--------------------------------------------------------------------------------
 1 | 
 7 | 
 8 | 
20 | 
36 | 


--------------------------------------------------------------------------------
/front/src/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
  1 | 
 24 | 
 25 | 
 33 | 
104 | 


--------------------------------------------------------------------------------
/front/src/components/Tag/index.vue:
--------------------------------------------------------------------------------
  1 | 
 19 | 
 20 | 
 69 | 
116 | 


--------------------------------------------------------------------------------
/front/src/components/album/index.vue:
--------------------------------------------------------------------------------
 1 | 
19 | 
20 | 
56 | 
84 | 
85 | 
91 | 
92 | 


--------------------------------------------------------------------------------
/front/src/components/blog-pager/index.vue:
--------------------------------------------------------------------------------
  1 | 
 17 | 
 18 | 
 77 | 
147 | 


--------------------------------------------------------------------------------
/front/src/components/blog-summary/index.vue:
--------------------------------------------------------------------------------
 1 | 
15 | 
16 | 
35 | 
61 | 


--------------------------------------------------------------------------------
/front/src/components/musicplayer/part/aplayer-controller-volume.vue:
--------------------------------------------------------------------------------
  1 | 
 27 | 
 28 | 
109 | 
143 | 


--------------------------------------------------------------------------------
/front/src/components/musicplayer/part/aplayer-controller.vue:
--------------------------------------------------------------------------------
 1 | 
28 | 
29 | 
82 | 
95 | 


--------------------------------------------------------------------------------
/front/src/components/musicplayer/part/aplayer-iconbutton.vue:
--------------------------------------------------------------------------------
 1 | 
 9 | 
10 | 
24 | 
27 | 


--------------------------------------------------------------------------------
/front/src/components/musicplayer/part/aplayer-list.vue:
--------------------------------------------------------------------------------
 1 | 
26 | 
27 | 
55 | 
59 | 


--------------------------------------------------------------------------------
/front/src/components/musicplayer/part/aplayer-lrc.vue:
--------------------------------------------------------------------------------
 1 | 
17 | 
18 | 
89 | 
92 | 


--------------------------------------------------------------------------------
/front/src/components/musicplayer/part/aplayer-thumbnail.vue:
--------------------------------------------------------------------------------
 1 | 
16 | 
71 | 
88 | 


--------------------------------------------------------------------------------
/front/src/components/musicplayer/utils.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Parse lrc, suppose multiple time tag
  3 |  * @see https://github.com/MoePlayer/APlayer/blob/master/src/js/lrc.js#L83
  4 |  * @author DIYgod(https://github.com/DIYgod)
  5 |  *
  6 |  * @param {String} lrcS - Format:
  7 |  * [mm:ss]lyric
  8 |  * [mm:ss.xx]lyric
  9 |  * [mm:ss.xxx]lyric
 10 |  * [mm:ss.xx][mm:ss.xx][mm:ss.xx]lyric
 11 |  * [mm:ss.xx]lyric
 12 |  *
 13 |  * @return {String} [[time, text], [time, text], [time, text], ...]
 14 |  */
 15 | export function parseLrc(lrcS) {
 16 |   if (lrcS) {
 17 |     const reLrcS = lrcS.replace(/([^\]^\n])\[/g, (match, p1) => p1 + '\n[');
 18 |     const lyric = reLrcS.split('\n');
 19 |     const lrc = [];
 20 |     const lyricLen = lyric.length;
 21 |     for (let i = 0; i < lyricLen; i++) {
 22 |       // match lrc time
 23 |       const lrcTimes = lyric[i].match(/\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/g);
 24 |       // match lrc text
 25 |       const lrcText = lyric[i].replace(/.*\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/g, '').replace(/<(\d{2}):(\d{2})(\.(\d{2,3}))?>/g, '').replace(/^\s+|\s+$/g, '');
 26 |       if (lrcTimes) {
 27 |         // handle multiple time tag
 28 |         const timeLen = lrcTimes.length;
 29 |         for (let j = 0; j < timeLen; j++) {
 30 |           const oneTime = /\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/.exec(lrcTimes[j]);
 31 |           const min2sec = oneTime[1] * 60;
 32 |           const sec2sec = parseInt(oneTime[2], 10);
 33 |           const msec2sec = oneTime[4] ? parseInt(oneTime[4], 10) / ((String(oneTime[4])).length === 2 ? 100 : 1000) : 0;
 34 |           const lrcTime = min2sec + sec2sec + msec2sec;
 35 |           lrc.push([lrcTime, lrcText]);
 36 |         }
 37 |       }
 38 |     }
 39 |     // sort by time
 40 |     lrc.sort((a, b) => a[0] - b[0]);
 41 |     return lrc;
 42 |   } else {
 43 |     return [];
 44 |   }
 45 | }
 46 | /**
 47 |  * Compare two semantic versions(major.minor.patch)
 48 |  */
 49 | export function versionCompare(semantic1, semantic2) {
 50 |   if (semantic1 === semantic2) {
 51 |     return 0;
 52 |   }
 53 |   const [major1, minor1, patch1] = semantic1.split('.');
 54 |   const [major2, minor2, patch2] = semantic2.split('.');
 55 | 
 56 |   if (major1 > major2) {
 57 |     return 1;
 58 |   } else if (major1 === major2) {
 59 |     if (minor1 > minor2) {
 60 |       return 1;
 61 |     } else if (minor1 === minor2) {
 62 |       if (patch1 > patch2) {
 63 |         return 1;
 64 |       }
 65 |     }
 66 |   }
 67 |   return -1;
 68 | }
 69 | 
 70 | export function warn(message) {
 71 |   return console.warn(`[Vue-APlayer] ${message}`);
 72 | }
 73 | 
 74 | export function deprecatedProp(name, sinceVersion, alternative) {
 75 |   return warn(`'${name}' is deprecated since v${sinceVersion}, and will be removed in future releases, use '${alternative}' instead`);
 76 | }
 77 | 
 78 | export function getElementViewLeft(element) {
 79 |   let actualLeft = element.offsetLeft;
 80 |   let current = element.offsetParent;
 81 |   let elementScrollLeft;
 82 |   while (current !== null) {
 83 |     actualLeft += current.offsetLeft;
 84 |     current = current.offsetParent;
 85 |   }
 86 |   elementScrollLeft = document.body.scrollLeft + document.documentElement.scrollLeft;
 87 |   return actualLeft - elementScrollLeft;
 88 | }
 89 | 
 90 | export function getElementViewTop(element) {
 91 |   let actualTop = element.offsetTop;
 92 |   let current = element.offsetParent;
 93 |   let elementScrollTop;
 94 |   while (current !== null) {
 95 |     actualTop += current.offsetTop;
 96 |     current = current.offsetParent;
 97 |   }
 98 |   elementScrollTop = document.body.scrollTop + document.documentElement.scrollTop;
 99 |   return actualTop - elementScrollTop;
100 | }
101 | 


--------------------------------------------------------------------------------
/front/src/components/page-container/index.vue:
--------------------------------------------------------------------------------
 1 | 
 4 | 
44 | 
47 | 


--------------------------------------------------------------------------------
/front/src/components/post-container/index.vue:
--------------------------------------------------------------------------------
 1 | 
 4 | 
 5 | 
62 | 
65 | 


--------------------------------------------------------------------------------
/front/src/components/second-title/img/mask.svg:
--------------------------------------------------------------------------------
1 | 


--------------------------------------------------------------------------------
/front/src/components/second-title/index.vue:
--------------------------------------------------------------------------------
 1 | 
11 | 
19 | 
48 | 


--------------------------------------------------------------------------------
/front/src/components/sidebar/img/rss.svg:
--------------------------------------------------------------------------------
1 | rss


--------------------------------------------------------------------------------
/front/src/components/tag-pager/index.vue:
--------------------------------------------------------------------------------
 1 | 
12 | 
13 | 
70 | 
86 | 


--------------------------------------------------------------------------------
/front/src/main.ts:
--------------------------------------------------------------------------------
 1 | // The Vue build version to load with the `import` command
 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
 3 | import Vue from 'vue';
 4 | import App from './App';
 5 | import Sidebar from '@/components/sidebar';
 6 | import { createStore } from './store/store';
 7 | import { createRouter } from './router';
 8 | import { sync } from 'vuex-router-sync';
 9 | import 'normalize.css';
10 | 
11 | const isProd = process.env.NODE_ENV === 'production';
12 | 
13 | Vue.config.productionTip = false;
14 | 
15 | /* eslint-disable no-new */
16 | 
17 | export function createApp() {
18 |   const store = createStore();
19 |   const router = createRouter();
20 |   sync(store, router);
21 |   const app = new Vue({
22 |     router,
23 |     store,
24 |     render: h => h(App)
25 |   });
26 |   const preFetchComponent = [
27 |     Sidebar,
28 |     App
29 |   ];
30 |   return { app, router, store, preFetchComponent, isProd };
31 | }
32 | 
33 | 


--------------------------------------------------------------------------------
/front/src/mixin/disqus.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/10.
 3 |  */
 4 | import Vue from 'vue';
 5 | import { Watch, Component } from 'vue-property-decorator';
 6 | const TYPES = ['post', 'page'];
 7 | 
 8 | @Component
 9 | export default class DisqusMixin extends Vue {
10 |   @Watch('$route')
11 |   public resetDisqus(val, oldVal) {
12 |     if (TYPES.indexOf(val.name) === -1) return;
13 |     if (val.path === oldVal.path) return;
14 |     if (window.DISQUS) {
15 |       this.reset(window.DISQUS);
16 |     }
17 |   }
18 | 
19 |   public reset(dsq) {
20 |     dsq.reset({
21 |       reload: true,
22 |       config: function () {
23 |         this.page.identifier = (self.$route.path || window.location.pathname);
24 |         this.page.url = window.location.href;
25 |       }
26 |     });
27 |   }
28 | }
29 | 


--------------------------------------------------------------------------------
/front/src/mixin/image.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/17.
 3 |  */
 4 | import Vue from 'vue';
 5 | import { Component } from 'vue-property-decorator';
 6 | import { Getter } from 'vuex-class';
 7 | 
 8 | @Component
 9 | export default class MyMixin extends Vue {
10 |   @Getter public option
11 |   @Getter public siteInfo
12 |   @Getter public supportWebp
13 |   public get logoUrl() {
14 |     return this.getValidImageUrl(this.option ? this.option.logoUrl || '' : '');
15 |   }
16 |   public get sidebarUrl() {
17 |     return this.getValidImageUrl(this.option ? this.option.sidebarImageUrl || '' : '');
18 |   }
19 |   public getValidImageUrl(url) {
20 |     if (!this.supportWebp) return url.replace(/.webp$/, '.png').replace('/webp', '');
21 |     return url;
22 |   }
23 | }
24 | 


--------------------------------------------------------------------------------
/front/src/router/index.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue';
 2 | import Router from 'vue-router';
 3 | import VueMeta from 'vue-meta';
 4 | 
 5 | const isServer = process.env.VUE_ENV === 'server';
 6 | 
 7 | if (isServer) {
 8 |   window.scrollTo = function(x, y) {
 9 |       // hack服务端scroll报错
10 |   };
11 | }
12 | 
13 | const BlogPager = () => import('@/components/blog-pager');
14 | const PostContainer = () => import('@/components/post-container');
15 | const Archive = () => import('@/components/archive');
16 | const Tag = () => import('@/components/tag');
17 | const TagPager = () => import('@/components/tag-pager');
18 | const About = () => import('@/components/about');
19 | const Album = () => import('@/components/album');
20 | const Link = () => import('@/components/link');
21 | const CatchMe = () => import('@/components/catchme');
22 | 
23 | Vue.use(Router);
24 | Vue.use(VueMeta);
25 | 
26 | export function createRouter() {
27 |   let router = new Router({
28 |     mode: isServer ? 'abstract' : 'history',
29 |     fallback: false,
30 |     scrollBehavior: function (to, from, savedPosition) {
31 |       if (to.hash) {
32 |         return { selector: decodeURIComponent(to.hash) };
33 |       }
34 |       if (savedPosition) {
35 |         return savedPosition;
36 |       } else {
37 |         return { x: 0, y: 0 };
38 |       }
39 |     },
40 |     routes: [
41 |       {
42 |         path: '/',
43 |         name: 'main',
44 |         component: BlogPager
45 |       },
46 |       {
47 |         path: '/post/:pathName',
48 |         name: 'post',
49 |         component: PostContainer
50 |       },
51 |       {
52 |         path: '/archive',
53 |         name: 'archive',
54 |         component: Archive
55 |       },
56 |       {
57 |         path: '/about',
58 |         name: 'about',
59 |         component: About
60 |       },
61 |       {
62 |         path: '/album',
63 |         name: 'album',
64 |         component: Album
65 |       },
66 |       {
67 |         path: '/link',
68 |         name: 'link',
69 |         component: Link
70 |       },
71 |       {
72 |         path: '/catchme',
73 |         name: 'catchme',
74 |         component: CatchMe
75 |       },
76 |       {
77 |         path: '/tag',
78 |         name: 'tag',
79 |         component: Tag
80 |       },
81 |       {
82 |         path: '/tag/:tagName',
83 |         name: 'tagPager',
84 |         component: TagPager
85 |       }
86 |     ]
87 |   });
88 |   return router;
89 | }
90 | 


--------------------------------------------------------------------------------
/front/src/server-entry.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/9/20.
 3 |  */
 4 | import { createApp } from './main';
 5 | 
 6 | const isDev = process.env.NODE_ENV !== 'production';
 7 | 
 8 | export default context => {
 9 |   return new Promise((resolve, reject) => {
10 |     const s = isDev && Date.now();
11 |     const { app, router, store, preFetchComponent } = createApp();
12 |     const { url } = context;
13 |     const { fullPath } = router.resolve(url).route;
14 | 
15 |     if (fullPath !== url) {
16 |       return reject({ url: fullPath });
17 |     }
18 | 
19 |     router.push(url);
20 |     context.meta = app.$meta();
21 | 
22 |     store.state.supportWebp = context.supportWebp;
23 | 
24 |     router.onReady(() => {
25 |       const matchedComponents = router.getMatchedComponents();
26 |       if (!matchedComponents.length) return reject({ code: 404 });
27 |       Promise.all(preFetchComponent.concat(matchedComponents).map(({ options: { asyncData }}) => asyncData && asyncData({
28 |         store,
29 |         route: router.currentRoute
30 |       }))).then(() => {
31 |         isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`);
32 |         context.state = store.state;
33 |         resolve(app);
34 |       }).catch(reject);
35 |     }, reject => {
36 |       console.error(Date.now().toLocaleString(), reject);
37 |     });
38 |   });
39 | };
40 | 


--------------------------------------------------------------------------------
/front/src/store/create-api-client.js:
--------------------------------------------------------------------------------
1 | export default {
2 |   host: '/proxyPrefix'
3 | };
4 | 


--------------------------------------------------------------------------------
/front/src/store/create-api-server.js:
--------------------------------------------------------------------------------
1 | /**
2 |  * Created by unsad on 2017/11/15.
3 |  */
4 | const isProd = process.env.NODE_ENV === 'production';
5 | 
6 | export default {
7 |   host: isProd ? `http://${process.env.serverHost}:3000` : 'http://localhost:8080/proxyPrefix'
8 | };
9 | 


--------------------------------------------------------------------------------
/front/src/store/index.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/5/23.
 3 |  */
 4 | import axios from 'axios';
 5 | import api from 'create-api';
 6 | 
 7 | const prefix = `${api.host}/api`;
 8 | 
 9 | const store = {};
10 | store.fetch = (model, query) => {
11 |   const target = `${prefix}/${model}`;
12 |   return axios.get(target, { params: query }).then(response => response.data);
13 | };
14 | 
15 | export default store;
16 | 
17 | 


--------------------------------------------------------------------------------
/front/src/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | /// 
2 | 
3 | declare interface Window {
4 |   __INITIAL_STATE__: any;
5 |   DISQUS: any;
6 |   disqus_config: any;
7 | }
8 | 
9 | 


--------------------------------------------------------------------------------
/front/src/typings/vue-shims.d.ts:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue';
 2 | declare module 'src/components/*' {
 3 |   export default Vue;
 4 | }
 5 | 
 6 | declare module '*.vue' {
 7 |   export default Vue;
 8 | }
 9 | 
10 | declare module 'vue/types/vue' {
11 |   interface Vue {
12 |     $meta?: () => any;
13 |     siteInfo?: any;
14 |   }
15 |   interface VueConstructor {
16 |     asyncData?: any;
17 |     version: any;
18 |   }
19 | }
20 | 
21 | declare module 'vue/types/options' {
22 |   interface ComponentOptions {
23 |     asyncData?: any 
24 |   }
25 | }


--------------------------------------------------------------------------------
/front/src/utils/404.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/6.
 3 |  */
 4 | export default {
 5 |   pathName: 404,
 6 |   createdAt: '2017-01-01 00:00:00',
 7 |   updatedAt: '2017-01-01 00:00:00',
 8 |   title: '404 Not Found',
 9 |   toc: '',
10 |   content: '不存在'
11 | };
12 | 


--------------------------------------------------------------------------------
/front/src/utils/clientGoogleAnalyse.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/5.
 3 |  */
 4 | export default function (fullPath) {
 5 |   let screen = window.screen;
 6 |   let params = {
 7 |     dt: document.title,
 8 |     dr: fullPath,
 9 |     ul: navigator.language || navigator.browserLanguage || '',
10 |     sd: screen.colorDepth + '-bit',
11 |     sr: screen.width + 'x' + screen.height,
12 |     dpr: window.devicePixelRatio || window.webkitDevicePixelRatio || window.mozDevicePixelRatio || 1,
13 |     dp: fullPath,
14 |     z: Number(new Date())
15 |   };
16 | 
17 |   let queryArr = [];
18 |   for (let i of Object.keys(params)) {
19 |     queryArr.push(i + '=' + encodeURIComponent(params[i]));
20 |   }
21 |   let queryString = '?' + queryArr.join('&');
22 |   window.ga_image = new window.Image();
23 |   window.ga_image.src = '/_.gif' + queryString;
24 | }
25 | 


--------------------------------------------------------------------------------
/front/src/utils/utils.js:
--------------------------------------------------------------------------------
 1 | export default {
 2 |   shuffle(arr) {
 3 |     for (let i = arr.length - 1; i >= 0; i--) {
 4 |       const randomIndex = Math.floor(Math.random() * (i + 1));
 5 |       const itemAtIndex = arr[randomIndex];
 6 |       arr[randomIndex] = arr[i];
 7 |       arr[i] = itemAtIndex;
 8 |     }
 9 |     return arr;
10 |   }
11 | };
12 | 


--------------------------------------------------------------------------------
/front/src/views/CreatePostView.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/14.
 3 |  */
 4 | import { mapGetters } from 'vuex';
 5 | import Post from '../components/Post.vue';
 6 | import mock404 from '../utils/404';
 7 | 
 8 | export default function (type) {
 9 |   const isPost = type === 'post';
10 |   const action = isPost ? 'FETCH_BLOG' : 'FETCH_PAGE';
11 |   const regExp = isPost ? /^\/post\//g : /^\//g;
12 |   const select = isPost ? { tags: 1, category: 1 } : {};
13 |   return {
14 |     metaInfo () {
15 |       return {
16 |         title: this.post.title
17 |       };
18 |     },
19 |     name: `${type}-view`,
20 |     computed: {
21 |       ...mapGetters([
22 |         'prev',
23 |         'next',
24 |         'siteInfo'
25 |       ]),
26 |       post () {
27 |         const target = isPost ? this.$store.state.blog : this.$store.state.page;
28 |         return target.pathName ? target : mock404;
29 |       }
30 |     },
31 |     asyncData({store, route: { path: pathName, params, query }}, callback) {
32 |       pathName = decodeURIComponent(pathName.replace(regExp, ''));
33 |       return store.dispatch(action, {
34 |         model: 'post',
35 |         query: {
36 |           conditions: {
37 |             pathName,
38 |             isPublic: true,
39 |             type
40 |           },
41 |           select: Object.assign({
42 |             title: 1,
43 |             createdAt: 1,
44 |             content: 1,
45 |             toc: 1,
46 |             updatedAt: 1,
47 |             pathName: 1,
48 |             allowComment: 1
49 |           }, select)
50 |         },
51 |         callback
52 |       });
53 |     },
54 |     render (h) {
55 |       return h(Post, {
56 |         props: {
57 |           type,
58 |           post: this.post,
59 |           prev: this.prev,
60 |           next: this.next,
61 |           siteInfo: this.siteInfo
62 |         }
63 |       });
64 |     }
65 |   };
66 | }
67 | 


--------------------------------------------------------------------------------
/front/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unsad/ssr-blog/8e5a4d84657f8b3b71362a46ed7322bd5e189863/front/static/favicon.ico


--------------------------------------------------------------------------------
/front/static/title_base.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unsad/ssr-blog/8e5a4d84657f8b3b71362a46ed7322bd5e189863/front/static/title_base.png


--------------------------------------------------------------------------------
/front/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "experimentalDecorators": true,
 4 |     "allowJs": true,
 5 |     "target": "es6",
 6 |     "noImplicitThis": true,
 7 |     "module": "es2015",
 8 |     "moduleResolution": "node",
 9 |     "allowSyntheticDefaultImports": true,
10 |     "pretty": true,
11 |     "strictFunctionTypes": false
12 |   },
13 |   "include": [
14 |     "./src/**/*"
15 |   ],  
16 |   "lib": [
17 |     "dom",
18 |     "es5",
19 |     "es6",
20 |     "es7",
21 |     "es2015"
22 |   ],
23 |   "baseUrl": ".",
24 |   "paths": {
25 |     "@": ["src"] 
26 |   }
27 | }


--------------------------------------------------------------------------------
/id_rsa.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unsad/ssr-blog/8e5a4d84657f8b3b71362a46ed7322bd5e189863/id_rsa.enc


--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
 1 | events {
 2 |   worker_connections 1024;
 3 | }
 4 | http {
 5 |   resolver 127.0.0.11 valid=5s ipv6=off;
 6 |   server {
 7 |     listen 80;
 8 |     server_name localhost;
 9 |     set $node_port 3000;
10 |     set $ssr_port 8080;
11 |     set $web_front web_front;
12 |     set $web_back web_back;
13 |     location ^~ / {
14 |       proxy_set_header X-Real-IP $remote_addr;
15 |       proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
16 |       proxy_set_header Host $http_host;
17 |       proxy_set_header X-Nginx-Proxy true;
18 |       proxy_pass http://$web_front:$ssr_port;
19 |       proxy_redirect off;
20 |     }
21 | 
22 |     location ^~ /proxyPrefix/ {
23 |     rewrite ^/proxyPrefix/(.*) /$1 break;
24 |     proxy_http_version 1.1;
25 |     proxy_pass http://$web_back:$node_port;
26 |     proxy_redirect off;
27 |     }
28 |   }
29 | }
30 | 


--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
 1 | # Created by .ignore support plugin (hsz.mobi)
 2 | ### Example user template template
 3 | ### Example user template
 4 | 
 5 | # IntelliJ project files
 6 | .idea
 7 | *.iml
 8 | out
 9 | gen
10 | node_modules/*
11 | npm-debug.log
12 | 


--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
 1 | 
 2 | FROM node:7.7
 3 | 
 4 | # copy all files to target
 5 | COPY . /app
 6 | RUN npm i -g pm2
 7 | RUN cd /app && npm install
 8 | 
 9 | # clean cache
10 | RUN npm cache clean
11 | 
12 | WORKDIR /app
13 | 
14 | EXPOSE 3000
15 | 
16 | CMD ["pm2-docker", "start", "pm2.json", "--env", "production"] 


--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/5/9.
 3 |  */
 4 | global.Promise = require('bluebird');
 5 | 
 6 | const Koa = require('koa');
 7 | const log = require('./utils/log');
 8 | const mongoRest = require('./mongoRest');
 9 | const models = require('./model/mongo');
10 | const redis = require('./model/redis');
11 | const config = require('./conf/config');
12 | 
13 | const configName = process.env.NODE_ENV === '"development"' ? 'dev' : 'prod';
14 | 
15 | const blogpackConfig = require(`./build/blogpack.${configName}.config`);
16 | blogpackConfig.models = models;
17 | blogpackConfig.redis = redis;
18 | const Blogpack = require('./blogpack');
19 | const lifecycle = global.lifecycle = new Blogpack(blogpackConfig);
20 | 
21 | const app = new Koa();
22 | const router = require('koa-router')();
23 | 
24 | (async () => {
25 |   try {
26 |     await lifecycle.beforeUseRoutes({
27 |       config: lifecycle.config,
28 |       app,
29 |       router,
30 |       models,
31 |       redis
32 |     })
33 | 
34 |     const beforeRestfulRoutes = lifecycle.getBeforeRestfulRoutes();
35 |     const afterRestfulRoutes = lifecycle.getAfterRestfulRoutes();
36 | 
37 |     const middlewareRoutes = await lifecycle.getMiddlewareRoutes();
38 | 
39 |     for (const item of middlewareRoutes) {
40 |       const middlewares = [...item.middleware];
41 |       item.needBeforeRoutes && middlewares.unshift(...beforeRestfulRoutes);
42 |       item.needAfterRoutes && middlewares.push(...afterRestfulRoutes);
43 | 
44 |       router[item.method](item.path, ...middlewares);
45 |     }
46 | 
47 |     Object.keys(models).map(name => models[name]).forEach(model => {
48 |       mongoRest(router, model, '/api', {
49 |         beforeRestfulRoutes,
50 |         afterRestfulRoutes
51 |       })
52 |     });
53 | 
54 |     app.use(router.routes());
55 | 
56 |     const beforeServerStartArr = lifecycle.getBeforeServerStartFuncs();
57 | 
58 | 
59 |     for (const middleware of beforeServerStartArr) {
60 |       await middleware();
61 |     }
62 |     app.listen(config.serverPort, () => {
63 |       log.info(`koa2 is running at ${config.serverPort}`)
64 |     });
65 |   } catch (err) {
66 |     log.error(err)
67 |   }
68 | })();
69 | 


--------------------------------------------------------------------------------
/server/blogpack.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/21.
 3 |  */
 4 | class blogpack {
 5 |   constructor(options) {
 6 |     this.config = options.config || {}
 7 |     this.plugins = options.plugins || []
 8 |     this.models = options.models
 9 |     this.redis = options.redis
10 |   }
11 | 
12 |   async beforeUseRoutes(...args) {
13 |     for (const plugin of this.plugins) {
14 |       plugin.beforeUseRoutes && await plugin.beforeUseRoutes(...args)
15 |     }
16 |   }
17 | 
18 |   async getMiddlewareRoutes(...args) {
19 |     const plugins = this.plugins.filter(plugin => plugin['mountingRoute']);
20 |     const result = [];
21 |     for (const plugin of plugins) {
22 |       const routeObj = await plugin.mountingRoute();
23 |       result.push(Object.assign({}, routeObj, {
24 |         needBeforeRoutes: routeObj.needBeforeRoutes || false,
25 |         needAfterRoutes: routeObj.needAfterRoutes || false
26 |       }));
27 |     }
28 |     return result;
29 |   }
30 | 
31 |   getBeforeRestfulRoutes() {
32 |     return this.plugins
33 |       .filter(plugin => plugin['beforeRestful'])
34 |       .map(plugin => plugin['beforeRestful'])
35 |   }
36 | 
37 |   getAfterRestfulRoutes() {
38 |     return this.plugins
39 |       .filter(plugin => plugin['afterRestful'])
40 |       .map(plugin => plugin['afterRestful'])
41 |   }
42 | 
43 |   getBeforeServerStartFuncs() {
44 |     return this.plugins
45 |       .filter(plugin => plugin['beforeServerStart'])
46 |       .map(plugin => plugin['beforeServerStart'])
47 |   }
48 | }
49 | 
50 | module.exports = blogpack


--------------------------------------------------------------------------------
/server/build/blogpack.base.config.js:
--------------------------------------------------------------------------------
1 | /**
2 |  * Created by unsad on 2017/11/21.
3 |  */
4 | var config = require('../conf/config')
5 | 
6 | module.exports = {
7 |   config,
8 |   plugins: []
9 | }


--------------------------------------------------------------------------------
/server/build/blogpack.dev.config.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/21.
 3 |  */
 4 | const base = require('./blogpack.base.config')
 5 | const useRoutesPrefix = '../plugins/beforeUseRoutes'
 6 | const serverStartPrefix = '../plugins/beforeServerStart'
 7 | const env = process.env
 8 | 
 9 | const config = Object.assign({}, base)
10 | 
11 | const BodyParserPlugin = require(`${useRoutesPrefix}/bodyParser`)
12 | const LogTimePlugin = require(`${useRoutesPrefix}/logTime`)
13 | const RestcPlugin = require(`${useRoutesPrefix}/restc`)
14 | const InitOptionPlugin = require(`${serverStartPrefix}/initOption`)
15 | const InstallThemePlugin = require(`${serverStartPrefix}/installTheme`)
16 | const InitUserPlugin = require(`${serverStartPrefix}/initUser`)
17 | 
18 | const CheckAuthPlugin = require('../plugins/beforeRestful/checkAuth')
19 | 
20 | const QiniuUploadPlugin = require('../plugins/mountingRoute/qiniu')
21 | const LoginPlugin = require('../plugins/mountingRoute/login')
22 | const LogoutPlugin = require('../plugins/mountingRoute/logout')
23 | 
24 | config.plugins.push(
25 |   // beforeUseRoutes
26 |   new BodyParserPlugin(),
27 |   new LogTimePlugin(),
28 |   new RestcPlugin(),
29 | 
30 |   // beforeRestful
31 |   new CheckAuthPlugin(),
32 |   // moutingRoute
33 |   new QiniuUploadPlugin({
34 |     qiniuAccessKey: env.qiniuAccessKey || config.config.qiniuAccessKey,
35 |     qiniuSecretKey: env.qiniuSecretKey || config.config.qiniuSecretKey,
36 |     qiniuBucketHost: env.qiniuBucketHost || config.config.qiniuBucketHost,
37 |     qiniuBucketName: env.qiniuBucketName || config.config.qiniuBucketName,
38 |     qiniuPipeline: env.qiniuPipeline || config.config.qiniuPipeline
39 |   }),
40 |   new LoginPlugin(),
41 |   new LogoutPlugin(),
42 |   // beforeServerStart
43 |   new InitUserPlugin(),
44 |   new InstallThemePlugin(),
45 |   new InitOptionPlugin()
46 | )
47 | 
48 | module.exports = config


--------------------------------------------------------------------------------
/server/build/blogpack.prod.config.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/21.
 3 |  */
 4 | const blogpack = require('../blogpack')
 5 | const devConfig = require('./blogpack.dev.config')
 6 | const useRoutesPrefix = '../plugins/beforeUseRoutes'
 7 | 
 8 | const config = Object.assign({}, devConfig)
 9 | 
10 | const RatelimitPlugin = require(`${useRoutesPrefix}/ratelimit`)
11 | 
12 | config.plugins.unshift(
13 |   // beforeUseRoutes
14 |   new RatelimitPlugin({
15 |     duration: 10000,
16 |     errorMessage: 'Slow Down Your Request',
17 |     id: ctx => ctx.ip,
18 |     max: 100
19 |   })
20 | )
21 | 
22 | module.exports = config


--------------------------------------------------------------------------------
/server/conf/config.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/5/9.
 3 |  */
 4 | const env = process.env;
 5 | 
 6 | module.exports = {
 7 |   serverPort: env.serverPort || 3000,
 8 |   mongoHost: env.mongoHost || '127.0.0.1',
 9 |   mongoDatabase: env.mongoDatabase || 'blog',
10 |   mongoPort: env.mongoPort || 19999,
11 |   redisHost: env.redisHost || '127.0.0.1',
12 |   redisPort: env.redisPort || 6379,
13 |   redisPassword: env.redisPassword || '',
14 |   tokenSecret: env.tokenSecret || 'test',
15 |   tokenExpiresIn: env.tokenExpiresIn || 3600,
16 |   defaultAdminName: env.defaultAdminName || 'admin',
17 |   defaultAdminPassword: env.defaultAdminPassword || 'unsad',
18 |   qiniuAccessKey: 'KMTVIK74xzTFkJ_kFEKlD86q1hC7dg-lHaI2RTMm',
19 |   qiniuSecretKey: 'V13BAPuLiF5yQHup7MnPUz8ia0hJvXegfYiXD7f-',
20 |   qiniuBucketHost: '//src.sweetalkos.com',
21 |   qiniuBucketName: 'unsad',
22 |   qiniuPipeline: ''
23 | };


--------------------------------------------------------------------------------
/server/conf/option.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/8/29.
 3 |  */
 4 | module.exports = [
 5 |   {
 6 |     'key': 'analyzeCode',
 7 |     'value': ''
 8 |   },
 9 |   {
10 |     'key': 'commentType',
11 |     'value': 'disqus'
12 |   },
13 |   {
14 |     'key': 'commentName',
15 |     'value': ''
16 |   },
17 |   {
18 |     'key': 'description',
19 |     'value': ''
20 |   },
21 |   {
22 |     'key': 'faviconUrl',
23 |     'value': '/static/logo.jpg'
24 |   },
25 |   {
26 |     'key': 'logoUrl',
27 |     'value': '/static/logo.jpg'
28 |   },
29 |   {
30 |     'key': 'githubUrl',
31 |     'value': ''
32 |   },
33 |   {
34 |     'key': 'keywords',
35 |     'value': '',
36 |     'desc': '网站关键字'
37 |   },
38 |   {
39 |     'key': 'miitbeian',
40 |     'value': ''
41 |   },
42 |   {
43 |     'key': 'numPerPage',
44 |     'value': ''
45 |   },
46 |   {
47 |     'key': 'siteUrl',
48 |     'value': ''
49 |   },
50 |   {
51 |     'key': 'title',
52 |     'value': ''
53 |   },
54 |   {
55 |     'key': 'weiboUrl',
56 |     'value': ''
57 |   },
58 |   {
59 |     'key': 'twoFactorAuth',
60 |     'value': ''
61 |   }
62 | ];


--------------------------------------------------------------------------------
/server/model/mongo.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Created by unsad on 2017/5/9.
  3 |  */
  4 | const config = require('../conf/config');
  5 | const mongoose = require('mongoose');
  6 | const log = require('../utils/log');
  7 | mongoose.Promise = global.Promise;
  8 | let mongoUrl = `${config.mongoHost}:${config.mongoPort}/${config.mongoDatabase}`;
  9 | 
 10 | mongoose.connect(mongoUrl);
 11 | 
 12 | let db = mongoose.connection; // 监控数据库变化
 13 | db.on('error', (err) => log.error('connect error:', err));
 14 | db.once('open', () => log.info(`MongoDB is ready on ${mongoUrl}`));
 15 | 
 16 | let Schema = mongoose.Schema;
 17 | let ObjectId = Schema.ObjectId;
 18 | 
 19 | let post = new Schema({
 20 |   type: {type: String, default: ''},
 21 |   status: {type: Number, default: 0},
 22 |   title: String,
 23 |   pathName: {type: String, default: ''},
 24 |   summary: {type: String},
 25 |   markdownContent: {type: String},
 26 |   content: {type: String},
 27 |   markdownToc: {type: String, default: ''},
 28 |   toc: {type: String, default: ''},
 29 |   allowComment: {type: Boolean, default: true},
 30 |   createdAt: {type: String, default: ''},
 31 |   updatedAt: {type: String, default: ''},
 32 |   // 1为公开,0为不公开
 33 |   isPublic: {type: Boolean, default: true},
 34 |   commentNum: Number,
 35 |   options: Object,
 36 |   category: String,
 37 |   tags: Array
 38 | });
 39 | 
 40 | let category = new Schema({
 41 |   name: String,
 42 |   pathName: String
 43 | });
 44 | 
 45 | let tag = new Schema({
 46 |   name: String,
 47 |   pathName: String
 48 | });
 49 | 
 50 | let option = new Schema({
 51 |   key: String,
 52 |   value: Schema.Types.Mixed,
 53 |   desc: String
 54 | });
 55 | 
 56 | let theme = new Schema({
 57 |   name: String,
 58 |   author: String,
 59 |   option: Schema.Types.Mixed
 60 | });
 61 | 
 62 | let user = new Schema({
 63 |   name: String,
 64 |   displayName: String,
 65 |   password: String,
 66 |   email: String
 67 | });
 68 | 
 69 | let music = new Schema({
 70 |   title: String,
 71 |   author: String,
 72 |   url: String,
 73 |   lyric: String
 74 | });
 75 | 
 76 | let album = new Schema({
 77 |   type: String,
 78 |   url: String, 
 79 |   title: String
 80 | });
 81 | 
 82 | post = mongoose.model('post', post);
 83 | category = mongoose.model('category', category);
 84 | option = mongoose.model('option', option);
 85 | theme = mongoose.model('theme', theme);
 86 | tag = mongoose.model('tag', tag);
 87 | user = mongoose.model('user', user);
 88 | music = mongoose.model('music', music);
 89 | album = mongoose.model('album', album);
 90 | 
 91 | module.exports = {
 92 |   post,
 93 |   category,
 94 |   option,
 95 |   tag,
 96 |   user,
 97 |   theme,
 98 |   music,
 99 |   album
100 | };
101 | 
102 | 
103 | 


--------------------------------------------------------------------------------
/server/model/redis.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/8/11.
 3 |  */
 4 | const config = require('../conf/config');
 5 | const redis = require('redis');
 6 | const bluebird = require('bluebird');
 7 | const log = require('../utils/log');
 8 | 
 9 | bluebird.promisifyAll(redis.RedisClient.prototype);
10 | bluebird.promisifyAll(redis.Multi.prototype);
11 | 
12 | const auth = config.redisPassword ? { password: config.redisPassword } : {};
13 | 
14 | let client = redis.createClient(Object.assign({}, auth, {
15 |   host: config.redisHost,
16 |   port: config.redisPort
17 | }));
18 | 
19 | client.on('error', (err) => log.error('Redis Error', err));
20 | 
21 | client.on('connect', () => log.info('Redis is ready'));
22 | 
23 | module.exports = client;


--------------------------------------------------------------------------------
/server/mongoRest/actions.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/5/10.
 3 |  */
 4 | module.exports = function generateActions(model) {
 5 |   return {
 6 |     findAll: async function(ctx, next) {
 7 |       // next();
 8 |       try {
 9 |         let conditions = {};
10 |         let select = {};
11 |         let query = ctx.request.query;
12 |         if (query.conditions) {
13 |           conditions = JSON.parse(query.conditions);
14 |         }
15 |         let builder = model.find(conditions);
16 |         if (query.select) {
17 |           select = JSON.parse(query.select);
18 |           builder = builder.select(select);
19 |         }
20 |         ['limit', 'skip', 'sort', 'count'].forEach(function(key) {
21 |           if (query[key]) {
22 |             let arg = query[key];
23 |             if (key === 'limit' || key === 'skip') {
24 |               arg = parseInt(arg);
25 |             }
26 |             if (key === 'sort' && typeof arg === 'string') {
27 |               arg = JSON.parse(arg);
28 |             }
29 |             if (key !== 'count') {
30 |               builder[key](arg);
31 |             } else {
32 |               builder[key]();
33 |             }
34 |           }
35 |         });
36 |         const result = await builder.exec();
37 |         return ctx.body = result;
38 |       } catch (error) {
39 |         return ctx.body = error;
40 |       }
41 |     },
42 |     findById: async function(ctx, next) {
43 |       // await next;
44 |       try {
45 |         let select = {};
46 |         let query = ctx.request.query;
47 |         let builder = model.findById(ctx.params.id);
48 |         if (query.select) {
49 |           select = JSON.parse(query.select);
50 |           builder = builder.select(select);
51 |         }
52 |         const result = await builder.exec();
53 |         return ctx.body = result;
54 |       } catch (error) {
55 |         return ctx.body = error;
56 |       }
57 |     },
58 |     deleteById: async function(ctx, next) {
59 |       // await next;
60 |       try {
61 |         const result = await model.findByIdAndRemove(ctx.params.id).exec();
62 |         return ctx.body = result;
63 |       } catch (error) {
64 |         return ctx.body = error;
65 |       }
66 |     },
67 |     replaceById: async function(ctx, next) {
68 |      // await next;
69 |       try {
70 |         await model.findByIdAndRemove(ctx.params.id).exec();
71 |         const newDocument = ctx.request.body;
72 |         newDocument._id = ctx.params.id;
73 |         const result = await model.create(newDocument);
74 |         return ctx.body = result;
75 |       } catch (error) {
76 |         return ctx.body = error;
77 |       }
78 |     },
79 |     updateById: async function(ctx, next) {
80 |       // await next;
81 |       try {
82 |         const result = await model.findByIdAndUpdate(ctx.params.id, ctx.request.body, {new: true}).exec();
83 |         return ctx.body = result;
84 |       } catch (error) {
85 |         return ctx.body = error;
86 |       }
87 |     },
88 |     create: async function(ctx, next) {
89 |       await next;
90 |       try {
91 |         const result = await model.create(ctx.request.body);
92 |         ctx.status = 201;
93 |         return ctx.body = result;
94 |       } catch (error) {
95 |         return ctx.body = error;
96 |       }
97 |     }
98 |   }
99 | };


--------------------------------------------------------------------------------
/server/mongoRest/index.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/5/10.
 3 |  */
 4 | const generateRoutes = require('./routes');
 5 | const generateActions = require('./actions');
 6 | 
 7 | module.exports = (router, model, prefix, middlewares) => {
 8 |   const actions = generateActions(model);
 9 |   generateRoutes(router, model.modelName, actions, prefix, middlewares);
10 | };


--------------------------------------------------------------------------------
/server/mongoRest/routes.js:
--------------------------------------------------------------------------------
 1 | 
 2 | module.exports = (router, modelName, actions, prefix, {
 3 |   beforeRestfulRoutes,
 4 |   afterRestfulRoutes
 5 | }) => {
 6 |   const modelUrl = `${prefix}/${modelName}`;
 7 |   const itemUrl = `${prefix}/${modelName}/:id`;
 8 | 
 9 |   router.get(modelUrl, ...beforeRestfulRoutes, actions.findAll, ...afterRestfulRoutes);
10 |   router.get(itemUrl, ...beforeRestfulRoutes, actions.findById, ...afterRestfulRoutes);
11 |   router.post(modelUrl, ...beforeRestfulRoutes, actions.create, ...afterRestfulRoutes);
12 |   router.post(itemUrl, ...beforeRestfulRoutes, actions.updateById, ...afterRestfulRoutes);
13 |   router.del(itemUrl, ...beforeRestfulRoutes, actions.deleteById, ...afterRestfulRoutes);
14 |   router.put(modelUrl, ...beforeRestfulRoutes, actions.create, ...afterRestfulRoutes);
15 |   router.put(itemUrl, ...beforeRestfulRoutes, actions.replaceById, ...afterRestfulRoutes);
16 |   router.patch(itemUrl, ...beforeRestfulRoutes, actions.updateById, ...afterRestfulRoutes);
17 | };


--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "scripts": {
 3 |     "start": "cross-env mongoPort=27017 node app.js"
 4 |   },
 5 |   "dependencies": {
 6 |     "bluebird": "^3.5.0",
 7 |     "jsonwebtoken": "^7.4.2",
 8 |     "koa": "^2.2.0",
 9 |     "koa-bodyparser": "^4.2.0",
10 |     "koa-ratelimit": "^4.0.0",
11 |     "koa-router": "^7.1.1",
12 |     "log4js": "^1.1.1",
13 |     "mongoose": "^4.9.8",
14 |     "qiniu": "^7.0.9",
15 |     "redis": "^2.8.0",
16 |     "restc": "^0.2.2"
17 |   },
18 |   "devDependencies": {
19 |     "cross-env": "^5.1.3"
20 |   }
21 | }
22 | 


--------------------------------------------------------------------------------
/server/plugins/beforeRestful/checkAuth.js:
--------------------------------------------------------------------------------
 1 | const redis = require('../../model/redis');
 2 | const tokenService = require('../../service/token');
 3 | 
 4 | module.exports = class {
 5 |   async beforeRestful(ctx, next) {
 6 |     const isGettingUser = ctx.url.startsWith('/api/user');
 7 |     const isGettingAdmin = ctx.url.startsWith('/admin/');
 8 |     const isNotGet = ctx.url.startsWith('/api/') && ctx.method !== 'GET';
 9 |     if (!isGettingAdmin && !isGettingUser && !isNotGet) {
10 |       return next();
11 |     }
12 |     const headers = ctx.request.headers;
13 |     let token;
14 |     try {
15 |       token = headers['authorization'];
16 |     } catch (err) {
17 |       return ctx.body = {
18 |         status: 'fail',
19 |         description: err
20 |       };
21 |     }
22 |     if (!token) {
23 |       return ctx.body = {
24 |         status: 'fail',
25 |         description: 'Token not found'
26 |       };
27 |     }
28 |     const result = tokenService.verifyToken(token);
29 |     if (result === false) {
30 |       return ctx.body = {
31 |         status: 'fail',
32 |         description: 'Token verify failed'
33 |       };
34 |     }
35 |     let reply = await redis.getAsync('token');
36 |     if (reply === token) {
37 |       await next();
38 |     } else {
39 |       return ctx.body = {
40 |         status: 'fail',
41 |         description: 'token invalid'
42 |       };
43 |     }
44 |   }
45 | }


--------------------------------------------------------------------------------
/server/plugins/beforeServerStart/initOption.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/20.
 3 |  */
 4 | const log = require('../../utils/log')
 5 | const options = require('../../conf/option')
 6 | const models = require('../../model/mongo')
 7 | 
 8 | module.exports = class {
 9 |   async beforeServerStart() {
10 |     for (const option of options) {
11 |       let key = option.key
12 |       let count = await models.option.find({key}).count().exec()
13 |       if (count === 0) {
14 |         await models.option.create(option)
15 |         log.info(`Option ${key} created`)
16 |       }
17 |     }
18 |   }
19 | }


--------------------------------------------------------------------------------
/server/plugins/beforeServerStart/initUser.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/20.
 3 |  */
 4 | const log = require('../../utils/log')
 5 | const config = require('../../conf/config')
 6 | const models = require('../../model/mongo')
 7 | 
 8 | module.exports = class {
 9 |   async beforeServerStart() {
10 |     const count = await models.user.find().count().exec()
11 |     if (count !== 0) return
12 | 
13 |     if (config.defaultAdminPassword === 'admin') {
14 |       log.error('you must change the default passoword at ./conf/config.js')
15 |       log.error('koa2 refused to start because of weak password')
16 |       return process.exit(1)
17 |     }
18 | 
19 |     const result = await models.user.create({
20 |       name: config.defaultAdminName,
21 |       password: config.defaultAdminPassword,
22 |       displayName: config.defaultAdminName,
23 |       email: ''
24 |     })
25 | 
26 |     log.info(`account '${result.name}' is created`)
27 |   }
28 | }
29 | 


--------------------------------------------------------------------------------
/server/plugins/beforeServerStart/installTheme.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/20.
 3 |  */
 4 | const log = require('../../utils/log')
 5 | const fs = require('fs')
 6 | const path = require('path')
 7 | const resolve = file => path.resolve(__dirname, file)
 8 | const models = require('../../model/mongo')
 9 | 
10 | module.exports = class {
11 |   async beforeServerStart() {
12 |     const prefix = '../../theme'
13 |     let fileArr = fs.readdirSync(resolve(prefix))
14 |     for (let i = 0, len = fileArr.length; i < len; i++) {
15 |       let fileName = fileArr[i]
16 |       let theme = require(`${prefix}/${fileName}`)
17 |       let count = await models.theme.find({name: theme.name}).count().exec()
18 |       if (count === 0) {
19 |         await models.theme.create(theme)
20 |         log.info(`theme ${theme.name} created`)
21 |       }
22 |     }
23 |   }
24 | }
25 | 


--------------------------------------------------------------------------------
/server/plugins/beforeUseRoutes/bodyParser.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/21.
 3 |  */
 4 | const bodyParser = require('koa-bodyparser');
 5 | 
 6 | module.exports = class {
 7 |   async beforeUseRoutes({ app }) {
 8 |     app.use(bodyParser())
 9 |   }
10 | }


--------------------------------------------------------------------------------
/server/plugins/beforeUseRoutes/logTime.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/21.
 3 |  */
 4 | const log = require('../../utils/log');
 5 | 
 6 | module.exports = class {
 7 |   async beforeUseRoutes({ app, redis }) {
 8 |     app.use(async (ctx, next) => {
 9 |       const start = new Date();
10 |       await next();
11 |       const ms = new Date() - start;
12 |       log.info(`${ctx.method} ${decodeURIComponent(ctx.url)} - ${ms}ms`);
13 |     });
14 |   }
15 | };


--------------------------------------------------------------------------------
/server/plugins/beforeUseRoutes/ratelimit.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/21.
 3 |  */
 4 | const ratelimit = require('koa-ratelimit');
 5 | 
 6 | module.exports = class {
 7 |   constructor(options) {
 8 |     this.options = options
 9 |   }
10 | 
11 |   async beforeUseRoutes({app, redis}) {
12 |     const config = Object.assign({}, this.options, {
13 |       db: redis
14 |     });
15 |     app.use(ratelimit(config))
16 |   }
17 | }


--------------------------------------------------------------------------------
/server/plugins/beforeUseRoutes/restc.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/21.
 3 |  */
 4 | const restc = require('restc');
 5 | 
 6 | module.exports = class {
 7 |   async beforeUseRoutes({ app }) {
 8 |     app.use(restc.koa2());
 9 |   }
10 | };


--------------------------------------------------------------------------------
/server/plugins/mountingRoute/login.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/11/22.
 3 |  */
 4 | const redis = require('../../model/redis');
 5 | const tokenService = require('../../service/token');
 6 | const {user: model} = require('../../model/mongo');
 7 | 
 8 | module.exports = class {
 9 |   async mountingRoute() {
10 |     return {
11 |       method: 'post',
12 |       path: '/admin/login',
13 |       middleware: [middleware]
14 |     }
15 |   }
16 | };
17 | 
18 | async function middleware(ctx, next) {
19 |   let users, user;
20 | 
21 |   try {
22 |     users = await model.find({name: ctx.request.body.name}).exec();
23 |     user = {
24 |       name: users[0].name,
25 |       timestamp: (new Date()).valueOf()
26 |     };
27 | 
28 |     let password = users[0].password;
29 | 
30 |     if (password === ctx.request.body.password) {
31 |       let token = tokenService.createToken(user);
32 |       redis.set('token', token, 'EX', tokenService.expiresIn, () => {
33 | 
34 |       });
35 |       return ctx.body = {
36 |         token: token,
37 |         status: 'success'
38 |       };
39 |     } else {
40 |       return ctx.body = {
41 |         status: 'fail',
42 |         description: 'Get token failed.Check the password'
43 |       }
44 |     }
45 |   } catch (_error) {
46 |     return ctx.body = {
47 |       status: 'fail',
48 |       description: 'Get token failed.Check the name'
49 |     }
50 |   }
51 | }


--------------------------------------------------------------------------------
/server/plugins/mountingRoute/logout.js:
--------------------------------------------------------------------------------
 1 | const redis = require('../../model/redis');
 2 | const tokenService = require('../../service/token');
 3 | 
 4 | module.exports = class {
 5 |   async mountingRoute() {
 6 |     return {
 7 |       method: 'post',
 8 |       path: '/admin/logout',
 9 |       middleware: [middleware]
10 |     }
11 |   }
12 | };
13 | 
14 | 
15 | async function middleware(ctx, next) {
16 |   const headers = ctx.request.headers;
17 |   let token;
18 |   try {
19 |     token = headers['authorization'];
20 |   } catch (err) {
21 |     return ctx.body = {
22 |       status: 'fail',
23 |       description: err
24 |     };
25 |   }
26 | 
27 |   if (!token) {
28 |     return ctx.body = {
29 |       status: 'fail',
30 |       description: 'Token not found'
31 |     };
32 |   }
33 | 
34 |   const result = tokenService.verifyToken(token);
35 | 
36 |   if (result === false) {
37 |     return ctx.body = {
38 |       status: 'fail',
39 |       description: 'Token verify failed'
40 |     };
41 |   } else {
42 |     await redis.del('token');
43 |     return ctx.body = {
44 |       status: 'success',
45 |       description: 'Token delete!'
46 |     };
47 |   }
48 | }


--------------------------------------------------------------------------------
/server/plugins/mountingRoute/qiniu.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/10/16.
 3 |  */
 4 | let qiniu = require('qiniu');
 5 | 
 6 | const fops = 'imageMogr2/format/webp';
 7 | 
 8 | const policy = (name, fileName, {qiniuBucketName, qiniuPipeline}) => {
 9 |   let encoded = new Buffer(`${qiniuBucketName}:webp/${fileName}`).toString('base64');
10 |   const persist = {};
11 |   if (qiniuPipeline !== '') {
12 |     persist.persistentOps = `${fops}|saveas/${encoded}`;
13 |     persist.persistentPipeline = qiniuPipeline
14 |   }
15 |   return Object.assign({}, persist, {
16 |     scope: name,
17 |     deadline: new Date().getTime() + 600
18 |   });
19 | };
20 | 
21 | const getQiniuTokenFromFileName = (fileName, {
22 |   qiniuBucketName,
23 |   qiniuPipeline,
24 |   qiniuBucketHost
25 | }) => {
26 |   const key = `${qiniuBucketName}:${fileName}`;
27 |   const putPolicy = new qiniu.rs.PutPolicy(policy(key, fileName, {
28 |     qiniuPipeline,
29 |     qiniuBucketName
30 |   }));
31 |   const upToken = putPolicy.uploadToken();
32 | 
33 |   return {
34 |     upToken,
35 |     key,
36 |     bucketHost: qiniuBucketHost,
37 |     supportWebp: qiniuPipeline !== ''
38 |   }
39 | };
40 | 
41 | module.exports = class {
42 |   constructor(options) {
43 |     this.options = options;
44 |     qiniu.conf.ACCESS_KEY = this.options.qiniuAccessKey;
45 |     qiniu.conf.SECRET_KEY = this.options.qiniuSecretKey;
46 |   }
47 | 
48 |   async mountingRoute() {
49 |     return {
50 |       method: 'post',
51 |       path: '/admin/qiniu',
52 |       needBeforeRoutes: true,
53 |       middleware: [
54 |         ({request, response}, next) => {
55 |           return response.body = getQiniuTokenFromFileName(
56 |             request.body.key,
57 |             this.options
58 |           )
59 |         }
60 |       ],
61 |       needAfterRoutes: false
62 |     }
63 |   }
64 | };


--------------------------------------------------------------------------------
/server/pm2.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "apps": [
 3 |     {
 4 |       "name": "backend",
 5 |       "script": "app.js",
 6 |       "cwd": "",
 7 |       "exec_mode": "cluster",
 8 |       "instances": 0,
 9 |       "max_memory_restart": "256M",
10 |       "autorestart": true,
11 |       "node_args": [],
12 |       "args": [],
13 |       "env": {
14 |         "HOST": "localhost"
15 |       },
16 |       "env_production": {
17 |         "NODE_ENV": "production",
18 |         "redisHost": "redis",
19 |         "mongoHost": "mongo_db"
20 |       }
21 |     }
22 |   ]
23 | }


--------------------------------------------------------------------------------
/server/service/token.js:
--------------------------------------------------------------------------------
 1 | const jwt = require('jsonwebtoken');
 2 | const config = require('../conf/config');
 3 | 
 4 | let secret = config.tokenSecret;
 5 | 
 6 | let expiresIn = config.tokenExpiresIn;
 7 | 
 8 | module.exports = {
 9 |   createToken(userinfo) {
10 |     let token = jwt.sign(userinfo, secret, {
11 |       expiresIn
12 |     });
13 |     return token;
14 |   },
15 |   verifyToken(token) {
16 |     if (!token) {
17 |       return false;
18 |     }
19 |     try {
20 |       let result = jwt.verify(token, secret);
21 |       return result;
22 |     } catch (err) {
23 |       return false;
24 |     }
25 |   },
26 |   expiresIn
27 | };


--------------------------------------------------------------------------------
/server/theme/firekylin.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by unsad on 2017/10/26.
 3 |  */
 4 | module.exports = {
 5 |   name: 'firekylin',
 6 |   author: 'github.com/75team/firekylin',
 7 |   option: {
 8 |     logoUrl: '/static/logo.jpg',
 9 |     sidebarImageUrl: '',
10 |     sidebarMoveCss: '',
11 |     sidebarFontColor: '#fff !important',
12 |     menu: [{
13 |       "option": "home",
14 |       "url": "/",
15 |       "label": "首页",
16 |     },
17 |     {
18 |       "option": "archive",
19 |       "url": "/archive",
20 |       "label": "归档"
21 |     },
22 |     {
23 |       "option": "tags",
24 |       "url": "/tag",
25 |       "label": "标签"
26 |     }]
27 |   }
28 | }


--------------------------------------------------------------------------------
/server/utils/log.js:
--------------------------------------------------------------------------------
1 | /**
2 |  * Created by unsad on 2017/5/9.
3 |  */
4 | const log4js = require('log4js');
5 | const config = require('../conf/config');
6 | 
7 | let log = log4js.getLogger(config.mongoDatabase);
8 | 
9 | module.exports = log;


--------------------------------------------------------------------------------