├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .vscode ├── cSpell.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── Procfile ├── README.md ├── appveyor.yml ├── ava.config.js ├── client ├── assets │ ├── fonts │ │ ├── element-icons.eot │ │ ├── element-icons.svg │ │ ├── element-icons.ttf │ │ ├── element-icons.woff │ │ ├── icomoon.eot │ │ ├── icomoon.svg │ │ ├── icomoon.ttf │ │ ├── icomoon.woff │ │ └── style.css │ ├── img │ │ ├── avatar.svg │ │ ├── banner-bg.svg │ │ ├── exit.svg │ │ ├── hare-logo-small.svg │ │ ├── hare-logo.svg │ │ ├── hare.svg │ │ ├── login-bg.jpeg │ │ ├── logo.svg │ │ └── pwd.svg │ └── styles │ │ └── main.scss ├── components │ ├── Footer.vue │ ├── ForkThis.vue │ ├── Headbar.vue │ ├── Navbar.vue │ └── examples │ │ ├── activity │ │ └── NewActivity.vue │ │ └── charts │ │ ├── BarDemo.vue │ │ ├── DoughnutDemo.vue │ │ ├── LineDemo.vue │ │ ├── PieDemo.vue │ │ ├── ReactiveDemo.vue │ │ └── ScatterDemo.vue ├── layouts │ ├── default.vue │ ├── empty.vue │ └── error.vue ├── locales │ ├── en.json │ ├── examples │ │ ├── en.json │ │ ├── fr.json │ │ └── zh.json │ ├── fr.json │ └── zh.json ├── middleware │ └── check-auth.js ├── pages │ ├── about.vue │ ├── account │ │ └── token.vue │ ├── examples │ │ ├── activity │ │ │ ├── create.vue │ │ │ └── index.vue │ │ ├── charts.vue │ │ └── index.vue │ ├── index.vue │ └── login.vue ├── plugins │ ├── clipboard.client.js │ ├── element-ui.js │ ├── error-handler.client.js │ └── i18n.js ├── static │ ├── favicon.ico │ └── vue.ico ├── store │ ├── examples │ │ ├── activity.js │ │ └── index.js │ ├── index.js │ └── menu.js └── utils │ ├── bus.js │ ├── consts.js │ └── debounce.js ├── nuxt.config.js ├── package.json ├── renovate.json ├── server ├── app.js ├── locales │ ├── en.json │ └── zh.json ├── middlewares │ ├── content.js │ ├── errors.js │ ├── index.js │ ├── logger.js │ ├── response-time.js │ └── robots.js ├── models │ └── user.js ├── routes │ ├── auth.js │ ├── examples.js │ ├── index.js │ └── menu.js └── utils │ ├── consts.js │ ├── helpers.js │ └── translator.js ├── test ├── config.test.js ├── helpers │ └── create-nuxt.js ├── index.test.js └── login.test.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:lts 6 | working_directory: ~/hare 7 | # branches: 8 | # only: 9 | # - master 10 | # - dev 11 | steps: &steps 12 | - checkout 13 | - restore_cache: 14 | keys: 15 | - hare-deps-{{ checksum "yarn.lock" }} 16 | # fallback to using the latest cache if no exact match is found 17 | - hare-deps- 18 | - run: yarn 19 | - save_cache: 20 | paths: 21 | - node_modules 22 | key: hare-deps-{{ checksum "yarn.lock" }} 23 | - run: yarn test 24 | deploy: 25 | docker: 26 | - image: circleci/node:lts 27 | working_directory: ~/hare 28 | steps: 29 | - checkout 30 | - run: 31 | name: Deploy to Heroku 32 | command: | 33 | git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git dev:master 34 | 35 | workflows: 36 | version: 2 37 | commit: 38 | jobs: 39 | - build 40 | - deploy: 41 | requires: 42 | - build 43 | filters: 44 | branches: 45 | only: 46 | - dev 47 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint', 9 | ecmaFeatures: { 10 | legacyDecorators: true 11 | } 12 | }, 13 | extends: [ 14 | '@nuxtjs', 15 | 'plugin:nuxt/recommended' 16 | ], 17 | rules: { 18 | 'nuxt/no-cjs-in-config': 'off' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt build 2 | .nuxt 3 | .build 4 | dist 5 | .cache 6 | 7 | # Nuxt generate 8 | .generated 9 | 10 | # Auth0 config 11 | config.json 12 | 13 | # vscode 14 | .vscode/* 15 | !.vscode/settings.json 16 | !.vscode/tasks.json 17 | !.vscode/launch.json 18 | !.vscode/extensions.json 19 | !.vscode/cSpell.json 20 | 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Runtime data 29 | pids 30 | *.pid 31 | *.seed 32 | *.pid.lock 33 | 34 | # Directory for instrumented libs generated by jscoverage/JSCover 35 | lib-cov 36 | 37 | # Coverage directory used by tools like istanbul 38 | coverage 39 | 40 | # nyc test coverage 41 | .nyc_output 42 | 43 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 44 | .grunt 45 | 46 | # Bower dependency directory (https://bower.io/) 47 | bower_components 48 | 49 | # node-waf configuration 50 | .lock-wscript 51 | 52 | # Compiled binary addons (http://nodejs.org/api/addons.html) 53 | build/Release 54 | 55 | # Dependency directories 56 | node_modules/ 57 | jspm_packages/ 58 | 59 | # Typescript v1 declaration files 60 | typings/ 61 | 62 | # Optional npm cache directory 63 | .npm 64 | 65 | # Optional eslint cache 66 | .eslintcache 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | 80 | 81 | # cache files for sublime text 82 | *.tmlanguage.cache 83 | *.tmPreferences.cache 84 | *.stTheme.cache 85 | 86 | # workspace files are user-specific 87 | *.sublime-workspace 88 | 89 | # project files should be checked into the repository, unless a significant 90 | # proportion of contributors will probably not be using SublimeText 91 | # *.sublime-project 92 | 93 | # sftp configuration file 94 | sftp-config.json 95 | 96 | # Package control specific files 97 | Package Control.last-run 98 | Package Control.ca-list 99 | Package Control.ca-bundle 100 | Package Control.system-ca-bundle 101 | Package Control.cache/ 102 | Package Control.ca-certs/ 103 | Package Control.merged-ca-bundle 104 | Package Control.user-ca-bundle 105 | oscrypto-ca-bundle.crt 106 | bh_unicode_properties.cache 107 | 108 | # Sublime-github package stores a github token in this file 109 | # https://packagecontrol.io/packages/sublime-github 110 | GitHub.sublime-settings 111 | 112 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 113 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 114 | 115 | # All intellij settings 116 | .idea 117 | 118 | # User-specific stuff: 119 | # .idea/**/workspace.xml 120 | # .idea/**/tasks.xml 121 | # .idea/dictionaries 122 | 123 | # Sensitive or high-churn files: 124 | # .idea/**/dataSources/ 125 | # .idea/**/dataSources.ids 126 | # .idea/**/dataSources.xml 127 | # .idea/**/dataSources.local.xml 128 | # .idea/**/sqlDataSources.xml 129 | # .idea/**/dynamic.xml 130 | # .idea/**/uiDesigner.xml 131 | 132 | # Gradle: 133 | # .idea/**/gradle.xml 134 | # .idea/**/libraries 135 | 136 | # Mongo Explorer plugin: 137 | # .idea/**/mongoSettings.xml 138 | 139 | ## File-based project format: 140 | *.iws 141 | 142 | ## Plugin-specific files: 143 | 144 | # IntelliJ 145 | /out/ 146 | 147 | # mpeltonen/sbt-idea plugin 148 | .idea_modules/ 149 | 150 | # JIRA plugin 151 | atlassian-ide-plugin.xml 152 | 153 | # Cursive Clojure plugin 154 | .idea/replstate.xml 155 | 156 | .metadata 157 | bin/ 158 | tmp/ 159 | *.tmp 160 | *.bak 161 | *.swp 162 | *~.nib 163 | local.properties 164 | .settings/ 165 | .loadpath 166 | .recommenders 167 | 168 | # Eclipse Core 169 | .project 170 | 171 | # External tool builders 172 | .externalToolBuilders/ 173 | 174 | # Locally stored "Eclipse launch configurations" 175 | *.launch 176 | 177 | # sbteclipse plugin 178 | .target 179 | 180 | # Tern plugin 181 | .tern-project 182 | 183 | # TeXlipse plugin 184 | .texlipse 185 | 186 | # Code Recommenders 187 | .recommenders/ 188 | 189 | # webpackmonitor 190 | .monitor/ 191 | 192 | .DS_Store 193 | -------------------------------------------------------------------------------- /.vscode/cSpell.json: -------------------------------------------------------------------------------- 1 | // cSpell Settings 2 | { 3 | // Version of the setting file. Always 0.1 4 | "version": "0.1", 5 | // language - current active spelling language 6 | "language": "en", 7 | // words - list of words to be always considered correct 8 | "words": [ 9 | "nuxt", 10 | "moxios", 11 | "captcha", 12 | "vuex", 13 | "chunkhash", 14 | "clarkdo", 15 | "xmlify", 16 | "axios", 17 | "wechat", 18 | "headbar", 19 | "dataset", 20 | "datasets", 21 | "tooltip", 22 | "tooltips", 23 | "chartjs", 24 | "contenthash", 25 | "mixins", 26 | "consts", 27 | "params" 28 | ], 29 | // flagWords - list of words to be always considered incorrect 30 | // This is useful for offensive words and common spelling errors. 31 | // For example "hte" should be "the" 32 | "flagWords": [ 33 | "hte" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Hare Dev", 9 | "type": "node", 10 | "request": "launch", 11 | "protocol": "inspector", 12 | "program": "${workspaceRoot}/server/app", 13 | "stopOnEntry": false, 14 | "sourceMaps": true, 15 | "env": { 16 | "NODE_ENV": "development", 17 | "DEBUG": "nuxt:*" 18 | } 19 | }, 20 | { 21 | "name": "Test Cases", 22 | "type": "node", 23 | "request": "launch", 24 | "protocol": "inspector", 25 | "program": "${workspaceRoot}/node_modules/ava/profile.js", 26 | "stopOnEntry": false, 27 | "args": [ 28 | "test/index.test.js" 29 | ], 30 | "cwd": "${workspaceRoot}", 31 | "sourceMaps": true, 32 | "env": { 33 | "NODE_ENV": "production", 34 | "DEBUG": "nuxt:*" 35 | } 36 | }, 37 | { 38 | "name": "Hare Prod", 39 | "type": "node", 40 | "request": "launch", 41 | "protocol": "inspector", 42 | "program": "${workspaceRoot}/dist/server/app", 43 | "env": { 44 | "NODE_ENV": "production", 45 | "DEBUG": "nuxt:*" 46 | } 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "cSpell.enabled": true, 4 | "eslint.enable": true 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | # [1.0.0-alpha.0](https://github.com/clarkdo/hare/compare/v0.4.0...v1.0.0-alpha.0) (2019-05-10) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * captcha is svg ([dbb802f](https://github.com/clarkdo/hare/commit/dbb802f)) 11 | * confit test error ([cc42e10](https://github.com/clarkdo/hare/commit/cc42e10)) 12 | * empty file in windows server build ([7aae40a](https://github.com/clarkdo/hare/commit/7aae40a)) 13 | * login test ([54fb8c6](https://github.com/clarkdo/hare/commit/54fb8c6)) 14 | * not send req if validation failed ([e14bc9a](https://github.com/clarkdo/hare/commit/e14bc9a)) 15 | * server failure ([7d36d39](https://github.com/clarkdo/hare/commit/7d36d39)) 16 | * start server is in ts mode ([b6310f8](https://github.com/clarkdo/hare/commit/b6310f8)) 17 | * use element-ui@2.7.2 before [#15277](https://github.com/clarkdo/hare/issues/15277) released ([dc57db9](https://github.com/clarkdo/hare/commit/dc57db9)) 18 | * use node-9 in circleci till nuxt next release ([9cdf118](https://github.com/clarkdo/hare/commit/9cdf118)) 19 | 20 | 21 | ### Features 22 | 23 | * enable modern mode ([b267c53](https://github.com/clarkdo/hare/commit/b267c53)) 24 | * Refactor examples, add Français locale ([f3881ec](https://github.com/clarkdo/hare/commit/f3881ec)) 25 | * remove momentjs ([52918bd](https://github.com/clarkdo/hare/commit/52918bd)) 26 | * replace vue-clipboards with vue-clipboard2 ([fb700d7](https://github.com/clarkdo/hare/commit/fb700d7)) 27 | * upgrade element-ui to 2.8 ([4bf6176](https://github.com/clarkdo/hare/commit/4bf6176)) 28 | * upgrade nuxt to 2 ([d132e67](https://github.com/clarkdo/hare/commit/d132e67)) 29 | * upgrade vue-i18n to v8 ([00eec76](https://github.com/clarkdo/hare/commit/00eec76)) 30 | * use eslint-plugin-vue ([cdf9ae8](https://github.com/clarkdo/hare/commit/cdf9ae8)) 31 | 32 | 33 | 34 | 35 | # [0.4.0](https://github.com/clarkdo/hare/compare/v0.3.0...v0.4.0) (2018-03-08) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * duplicate id in menu ([e889541](https://github.com/clarkdo/hare/commit/e889541)) 41 | 42 | 43 | 44 | 45 | # [0.3.0](https://github.com/clarkdo/hare/compare/v0.2.3...v0.3.0) (2018-01-17) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * bump appveyor node engine ([cb62f57](https://github.com/clarkdo/hare/commit/cb62f57)) 51 | * circleci not build pr ([8f8a198](https://github.com/clarkdo/hare/commit/8f8a198)) 52 | 53 | 54 | ### Features 55 | 56 | * upgrade nuxt.js to next before 1.0.0 released ([e15d4b1](https://github.com/clarkdo/hare/commit/e15d4b1)) 57 | * use element-ui 2.0 ([fa16771](https://github.com/clarkdo/hare/commit/fa16771)) 58 | 59 | 60 | 61 | 62 | ## [0.2.3](https://github.com/clarkdo/hare/compare/v0.2.1...v0.2.3) (2017-12-09) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * error parsing appveyor.yml ([3eb5682](https://github.com/clarkdo/hare/commit/3eb5682)) 68 | * ignore prepublish when installing ([6d57eda](https://github.com/clarkdo/hare/commit/6d57eda)) 69 | * ignore unavailable vulnerabilities for 30 days ([9cff0b3](https://github.com/clarkdo/hare/commit/9cff0b3)) 70 | * re-generate heroku api key ([6d8efd7](https://github.com/clarkdo/hare/commit/6d8efd7)) 71 | * session missing issue ([2b3d32e](https://github.com/clarkdo/hare/commit/2b3d32e)) 72 | * session not saved issue ([9f71d76](https://github.com/clarkdo/hare/commit/9f71d76)) 73 | * switch off macos building for now due to unstable travis ([b2ecea9](https://github.com/clarkdo/hare/commit/b2ecea9)) 74 | 75 | 76 | ### Features 77 | 78 | * cache dependencies in ci ([4470bd9](https://github.com/clarkdo/hare/commit/4470bd9)) 79 | * upgrade vue-chartjs to 3.0.0 ([37ad277](https://github.com/clarkdo/hare/commit/37ad277)) 80 | * use circleci instead of travis for building ([fbe5d8f](https://github.com/clarkdo/hare/commit/fbe5d8f)) 81 | 82 | 83 | 84 | 85 | ## [0.2.2](https://github.com/clarkdo/hare/compare/v0.2.1...v0.2.2) (2017-11-17) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * error parsing appveyor.yml ([64fcf4a](https://github.com/clarkdo/hare/commit/64fcf4a)) 91 | * ignore prepublish when installing ([1656e9f](https://github.com/clarkdo/hare/commit/1656e9f)) 92 | * re-generate heroku api key ([30e0da5](https://github.com/clarkdo/hare/commit/30e0da5)) 93 | * session missing issue ([a01576b](https://github.com/clarkdo/hare/commit/a01576b)) 94 | * session not saved issue ([f9411e8](https://github.com/clarkdo/hare/commit/f9411e8)) 95 | * switch off macos building for now due to unstable travis ([daf86d4](https://github.com/clarkdo/hare/commit/daf86d4)) 96 | 97 | 98 | ### Features 99 | 100 | * cache dependencies in ci ([4580e5b](https://github.com/clarkdo/hare/commit/4580e5b)) 101 | * upgrade vue-chartjs to 3.0.0 ([40c75cd](https://github.com/clarkdo/hare/commit/40c75cd)) 102 | * use circleci instead of travis for building ([72f2a73](https://github.com/clarkdo/hare/commit/72f2a73)) 103 | 104 | 105 | 106 | 107 | ## [0.2.1](https://github.com/clarkdo/hare/compare/v0.2.0...v0.2.1) (2017-10-17) 108 | 109 | 110 | ### Bug Fixes 111 | 112 | * change build directory ([897dc92](https://github.com/clarkdo/hare/commit/897dc92)) 113 | * correct program in vsc debug ([a4c42d2](https://github.com/clarkdo/hare/commit/a4c42d2)) 114 | 115 | 116 | ### Features 117 | 118 | * publish to npm ([bffb278](https://github.com/clarkdo/hare/commit/bffb278)) 119 | 120 | 121 | 122 | 123 | # [0.2.0](https://github.com/clarkdo/hare/compare/v0.1.4...v0.2.0) (2017-10-17) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * color of overflowed menu mismatch ([4d5297f](https://github.com/clarkdo/hare/commit/4d5297f)) 129 | * specify a maintainer of the Dockerfile ([fdfa58e](https://github.com/clarkdo/hare/commit/fdfa58e)) 130 | 131 | 132 | ### Features 133 | 134 | * refactor: login page ([c4ab867](https://github.com/clarkdo/hare/commit/c4ab867)) 135 | * add heroku in travis building ([8c5334](https://github.com/clarkdo/hare/commit/8c5334)) 136 | * add standard-version ([2760b35](https://github.com/clarkdo/hare/commit/2760b35)) 137 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at clark.duxin@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | # For legacy version 4 | MAINTAINER "clark.duxin@gmail.com" 5 | LABEL maintainer="clark.duxin@gmail.com" 6 | 7 | # Create app directory 8 | RUN mkdir -p /usr/app 9 | WORKDIR /usr/app 10 | 11 | # Bundle app source 12 | COPY package.json ./ 13 | COPY node_modules ./node_modules/ 14 | COPY dist ./dist 15 | 16 | EXPOSE 3000 17 | CMD [ "yarn", "start" ] 18 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | # For legacy version 4 | MAINTAINER "clark.duxin@gmail.com" 5 | LABEL maintainer="clark.duxin@gmail.com" 6 | 7 | # Create app directory 8 | RUN mkdir -p /usr/app 9 | WORKDIR /usr/app 10 | 11 | # Bundle app source 12 | COPY . /usr/app/ 13 | 14 | # Install app dependencies 15 | RUN yarn 16 | 17 | EXPOSE 3000 18 | CMD [ "yarn", "dev" ] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present, Xin (Clark) Du 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | The MIT License (MIT) 24 | 25 | Copyright (c) 2016-2017 Sebastien Chopin ([@Atinux](https://github.com/Atinux)) & Alexandre Chopin ([@alexchopin](https://github.com/alexchopin)) 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in all 35 | copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 43 | SOFTWARE. 44 | 45 | The MIT License (MIT) 46 | 47 | Copyright (c) 2013-present, Yuxi (Evan) You 48 | 49 | Permission is hereby granted, free of charge, to any person obtaining a copy 50 | of this software and associated documentation files (the "Software"), to deal 51 | in the Software without restriction, including without limitation the rights 52 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 53 | copies of the Software, and to permit persons to whom the Software is 54 | furnished to do so, subject to the following conditions: 55 | 56 | The above copyright notice and this permission notice shall be included in 57 | all copies or substantial portions of the Software. 58 | 59 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 60 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 61 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 62 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 63 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 64 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 65 | THE SOFTWARE. 66 | 67 | (The MIT License) 68 | 69 | Copyright (c) 2016 Koa contributors 70 | 71 | Permission is hereby granted, free of charge, to any person obtaining 72 | a copy of this software and associated documentation files (the 73 | 'Software'), to deal in the Software without restriction, including 74 | without limitation the rights to use, copy, modify, merge, publish, 75 | distribute, sublicense, and/or sell copies of the Software, and to 76 | permit persons to whom the Software is furnished to do so, subject to 77 | the following conditions: 78 | 79 | The above copyright notice and this permission notice shall be 80 | included in all copies or substantial portions of the Software. 81 | 82 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 83 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 84 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 85 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 86 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 87 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 88 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 89 | 90 | The MIT License (MIT) 91 | 92 | Copyright (c) 2016 ElemeFE 93 | 94 | Permission is hereby granted, free of charge, to any person obtaining a copy 95 | of this software and associated documentation files (the "Software"), to deal 96 | in the Software without restriction, including without limitation the rights 97 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 98 | copies of the Software, and to permit persons to whom the Software is 99 | furnished to do so, subject to the following conditions: 100 | 101 | The above copyright notice and this permission notice shall be included in all 102 | copies or substantial portions of the Software. 103 | 104 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 105 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 106 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 107 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 108 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 109 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 110 | SOFTWARE. 111 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #  Application boilerplate based on Vue.js 2.x, Koa 2.x, Element-UI and Nuxt.js 2 | 3 | [](https://circleci.com/gh/clarkdo/hare) 4 | [](https://ci.appveyor.com/project/clarkdo/hare) 5 | [](https://snyk.io/test/github/clarkdo/hare) 6 | [](https://standardjs.com) 7 | [](https://github.com/eslint/eslint) 8 | [](https://github.com/clarkdo/hare/issues) 9 | [](https://github.com/clarkdo/hare/stargazers) 10 | [](https://raw.githubusercontent.com/clarkdo/hare/master/LICENSE) 11 | 12 | ## Installation 13 | 14 | ``` bash 15 | $ git clone git@github.com:clarkdo/hare.git 16 | $ cd hare 17 | # install dependencies 18 | $ yarn 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### Development 24 | 25 | ``` bash 26 | # serve with hot reloading at localhost:3000 27 | $ yarn dev 28 | ``` 29 | 30 | Go to [http://localhost:3000](http://localhost:3000) 31 | 32 | ### Testing 33 | 34 | ``` bash 35 | # configure ESLint as a tool to keep codes clean 36 | $ yarn lint 37 | # use ava as testing framework, mixed with jsdom 38 | $ yarn test 39 | ``` 40 | 41 | ### Production 42 | 43 | ``` bash 44 | # build for production and launch the server 45 | $ yarn build 46 | $ yarn start 47 | ``` 48 | 49 | ### Generate 50 | 51 | ``` bash 52 | # generate a static project 53 | $ yarn generate 54 | ``` 55 | 56 | ### Analyze 57 | 58 | ``` bash 59 | # build and launch the bundle analyze 60 | $ yarn analyze 61 | ``` 62 | 63 | ### Use PM 64 | 65 | #### Further more features refer: [PM2](https://github.com/Unitech/pm2) 66 | 67 | ``` bash 68 | # install pm2 globally 69 | $ yarn global add pm2 70 | # launch development server 71 | $ yarn dev:pm2 72 | # launch production server 73 | $ yarn start:pm2 74 | # Display all processes status 75 | $ pm2 ls 76 | # Show all information about app 77 | $ pm2 show hare 78 | # Display memory and cpu usage of each app 79 | $ pm2 monit 80 | # Display logs 81 | $ pm2 logs 82 | # Stop 83 | $ pm2 stop hare 84 | # Kill and delete 85 | $ pm2 delete hare 86 | ``` 87 | 88 | ### Docker Dev 89 | 90 | ``` bash 91 | # build image 92 | $ docker build -t hare-dev -f Dockerfile.dev ./ 93 | $ docker run -d -p 8888:3000 -e HOST=0.0.0.0 hare-dev 94 | ``` 95 | 96 | Go to [http://localhost:8888](http://locdoalhost:8888) 97 | 98 | ### Docker Production 99 | 100 | ``` bash 101 | # build bundle 102 | $ export NODE_ENV='' 103 | $ yarn 104 | $ yarn build 105 | # install production dependencies (remove devDependencies) 106 | $ yarn --prod 107 | # build image 108 | $ docker build -t hare-prod -f Dockerfile ./ 109 | $ docker run -d -p 8889:3000 -e HOST=0.0.0.0 hare-prod 110 | ``` 111 | 112 | Go to [http://localhost:8889](http://locdoalhost:8889) 113 | 114 | ## Documentation 115 | 116 | Vue.js documentation: [https://vuejs.org/](https://vuejs.org/) 117 | 118 | Nuxt.js documentation: [https://nuxtjs.org](https://nuxtjs.org) 119 | 120 | Element-UI documentation: [http://element.eleme.io](http://element.eleme.io/#/en-US) 121 | 122 | Koa documentation: [https://github.com/koajs/koa](https://github.com/koajs/koa) 123 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # branches to build 2 | branches: 3 | # whitelist 4 | only: 5 | - dev 6 | - master 7 | # blacklist 8 | except: 9 | - gh-pages 10 | skip_branch_with_pr: true 11 | max_jobs: 4 12 | 13 | # Test against the latest version of this Node.js version 14 | environment: 15 | matrix: 16 | - nodejs_version: "8" 17 | - nodejs_version: "9" 18 | 19 | platform: 20 | # - x86 21 | - x64 22 | 23 | cache: 24 | - "%LOCALAPPDATA%\\Yarn" 25 | - node_modules 26 | 27 | # Install scripts. (runs after repo cloning) 28 | install: 29 | # Get the latest stable version of Node.js or io.js 30 | - ps: Install-Product node $env:nodejs_version 31 | # install modules 32 | - yarn 33 | 34 | # Post-install test scripts. 35 | test_script: 36 | # Output useful info for debugging. 37 | - node --version 38 | - yarn --version 39 | # - yarn lint 40 | # run tests 41 | - yarn test 42 | 43 | # build_script: 44 | # # run build 45 | # - yarn build 46 | build: off 47 | -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | files: ['./test/*.test.js'], 3 | tap: false, 4 | serial: true, 5 | verbose: true 6 | } 7 | -------------------------------------------------------------------------------- /client/assets/fonts/element-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarkdo/hare/7bd071b469e5b320195e993f0194be531d8d7700/client/assets/fonts/element-icons.eot -------------------------------------------------------------------------------- /client/assets/fonts/element-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20120731 at Tue Sep 13 18:32:46 2016 6 | By admin 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 36 | 41 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /client/assets/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarkdo/hare/7bd071b469e5b320195e993f0194be531d8d7700/client/assets/fonts/element-icons.ttf -------------------------------------------------------------------------------- /client/assets/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarkdo/hare/7bd071b469e5b320195e993f0194be531d8d7700/client/assets/fonts/element-icons.woff -------------------------------------------------------------------------------- /client/assets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarkdo/hare/7bd071b469e5b320195e993f0194be531d8d7700/client/assets/fonts/icomoon.eot -------------------------------------------------------------------------------- /client/assets/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarkdo/hare/7bd071b469e5b320195e993f0194be531d8d7700/client/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /client/assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarkdo/hare/7bd071b469e5b320195e993f0194be531d8d7700/client/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /client/assets/fonts/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('icomoon.eot?h6xgdm'); 4 | src: url('icomoon.eot?h6xgdm#iefix') format('embedded-opentype'), 5 | url('icomoon.ttf?h6xgdm') format('truetype'), 6 | url('icomoon.woff?h6xgdm') format('woff'), 7 | url('icomoon.svg?h6xgdm#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | /* use !important to prevent issues with browser extensions that change fonts */ 14 | font-family: 'icomoon' !important; 15 | speak: none; 16 | font-style: normal; 17 | font-weight: normal; 18 | font-variant: normal; 19 | text-transform: none; 20 | line-height: 1; 21 | 22 | /* Better Font Rendering =========== */ 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | .icon-rate-face-off:before { 28 | content: "\e900"; 29 | } 30 | .icon-rate-face-1:before { 31 | content: "\e901"; 32 | } 33 | .icon-rate-face-2:before { 34 | content: "\e902"; 35 | } 36 | .icon-rate-face-3:before { 37 | content: "\e903"; 38 | } 39 | 40 | @font-face { 41 | font-family: 'elementdoc'; 42 | src: url('element-icons.eot?h6xgdm'); 43 | src: url('element-icons.eot?h6xgdm#iefix') format('embedded-opentype'), 44 | url('element-icons.ttf?h6xgdm') format('truetype'), 45 | url('element-icons.woff?h6xgdm') format('woff'), 46 | url('element-icons.svg?h6xgdm#element-icons') format('svg'); 47 | font-weight: normal; 48 | font-style: normal; 49 | } 50 | 51 | .element-icons { 52 | font-family:"elementdoc" !important; 53 | font-size:16px; 54 | font-style:normal; 55 | -webkit-font-smoothing: antialiased; 56 | -webkit-text-stroke-width: 0.2px; 57 | -moz-osx-font-smoothing: grayscale; 58 | } 59 | .icon-github:before { content: "\e603"; } 60 | .icon-wechat:before { content: "\e601"; } 61 | 62 | 63 | -------------------------------------------------------------------------------- /client/assets/img/avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 头像 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /client/assets/img/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 退出 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/assets/img/hare-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ]> 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 32 | 33 | 34 | 35 | Hare 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 189 | 263 | 337 | 411 | 485 | 559 | 571 | 572 | 573 | -------------------------------------------------------------------------------- /client/assets/img/hare.svg: -------------------------------------------------------------------------------- 1 | 资源 1 -------------------------------------------------------------------------------- /client/assets/img/login-bg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarkdo/hare/7bd071b469e5b320195e993f0194be531d8d7700/client/assets/img/login-bg.jpeg -------------------------------------------------------------------------------- /client/assets/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /client/assets/img/pwd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 密码 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /client/assets/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'assets/fonts/style.css'; 2 | html, 3 | body, 4 | header, 5 | #__nuxt { 6 | height: 100%; 7 | // max-width: 1440px; 8 | // max-height: 900px; 9 | margin: 0 auto; 10 | } 11 | #__layout{ 12 | height: 100%; 13 | } 14 | body { 15 | font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 16 | 'Microsoft YaHei', SimSun, sans-serif; 17 | overflow: auto; 18 | font-weight: 400; 19 | -webkit-font-smoothing: antialiased; 20 | } 21 | a { 22 | color: #4078c0; 23 | text-decoration: none; 24 | } 25 | button, 26 | input, 27 | select, 28 | textarea { 29 | font-family: inherit; 30 | font-size: inherit; 31 | line-height: inherit; 32 | color: inherit; 33 | } 34 | .main { 35 | min-height: 100%; 36 | } 37 | .demo { 38 | margin: 20px 0; 39 | } 40 | .hide { 41 | opacity: 0 !important; 42 | width: 0 !important; 43 | } 44 | /* Nav Icon */ 45 | .nav-icon { 46 | $first-top: 5px; 47 | $space: 8px; 48 | width: 27px; 49 | height: 30px; 50 | position: relative; 51 | margin: 15px 0px 15px 5px; 52 | transform: rotate(0deg); 53 | transition: .5s ease-in-out; 54 | cursor: pointer; 55 | span { 56 | display: block; 57 | position: absolute; 58 | height: 2px; 59 | width: 100%; 60 | background: #324157; 61 | border-radius: 9px; 62 | opacity: 1; 63 | left: 0; 64 | transform: rotate(0deg); 65 | transition: .25s ease-in-out; 66 | &:nth-child(1) { 67 | top: $first-top; 68 | } 69 | &:nth-child(2), 70 | &:nth-child(3) { 71 | top: $first-top + $space; 72 | } 73 | &:nth-child(4) { 74 | top: $first-top + $space * 2; 75 | } 76 | } 77 | &.open span { 78 | &:nth-child(1) { 79 | top: $first-top + $space; 80 | width: 0%; 81 | left: 50%; 82 | } 83 | &:nth-child(2) { 84 | transform: rotate(45deg); 85 | } 86 | &:nth-child(3) { 87 | transform: rotate(-45deg); 88 | } 89 | &:nth-child(4) { 90 | top: $first-top + $space; 91 | width: 0%; 92 | left: 50%; 93 | } 94 | } 95 | } 96 | .input-with-select .el-input-group__prepend { 97 | width: auto 98 | } 99 | @media (max-width: 1440px) { 100 | .container { 101 | width: 100%; 102 | } 103 | } 104 | @media (max-width: 768px) { 105 | } 106 | -------------------------------------------------------------------------------- /client/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 2 | 30 | 31 | 32 | 41 | 42 | 137 | -------------------------------------------------------------------------------- /client/components/ForkThis.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 49 | -------------------------------------------------------------------------------- /client/components/Headbar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ displayName }} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {{ $t("head.pwd") }} 27 | 28 | 29 | 30 | 31 | 32 | {{ $t("head.exit") }} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 64 | 65 | 102 | -------------------------------------------------------------------------------- /client/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {{ menu.name }} 28 | 29 | 30 | 31 | {{ subMenu.name }} 32 | 33 | 34 | 35 | 36 | 37 | {{ menu.name }} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 81 | 82 | 127 | -------------------------------------------------------------------------------- /client/components/examples/activity/NewActivity.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | - 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | {{ $t('activity.create') }} 106 | 107 | 108 | {{ $t('activity.reset') }} 109 | 110 | 111 | 112 | 113 | 114 | 115 | 216 | 217 | 231 | -------------------------------------------------------------------------------- /client/components/examples/charts/BarDemo.vue: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /client/components/examples/charts/DoughnutDemo.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /client/components/examples/charts/LineDemo.vue: -------------------------------------------------------------------------------- 1 | 72 | -------------------------------------------------------------------------------- /client/components/examples/charts/PieDemo.vue: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /client/components/examples/charts/ReactiveDemo.vue: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /client/components/examples/charts/ScatterDemo.vue: -------------------------------------------------------------------------------- 1 | 62 | -------------------------------------------------------------------------------- /client/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 33 | 34 | 58 | -------------------------------------------------------------------------------- /client/layouts/empty.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /client/layouts/error.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ error.statusCode }} 6 | 7 | 8 | 9 | {{ error.message }} 10 | 11 | 12 | 13 | 14 | Back to the home page 15 | 16 | 17 | 18 | 19 | 20 | 21 | 39 | 40 | 82 | -------------------------------------------------------------------------------- /client/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": { 3 | "userRequired": "User Name is required", 4 | "userPlaceholder": "User Name", 5 | "pwdRequired": "Password is required", 6 | "pwdPlaceholder": "Password", 7 | "captchaRequired": "Captcha is required", 8 | "captchaPlaceholder": "Captcha", 9 | "login": "Login" 10 | }, 11 | "nav": { 12 | "home": "Home" 13 | }, 14 | "head": { 15 | "pwd": "Password", 16 | "exit": "Exit" 17 | }, 18 | "tagline": 19 | "Application boilerplate based on Vue.js 2.x, Koa 2.x, Element-UI, Axios, Vue i18n and Nuxt.js" 20 | } 21 | -------------------------------------------------------------------------------- /client/locales/examples/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "activity": { 3 | "title": { 4 | "create": "Create Activity" 5 | }, 6 | "account": "Account", 7 | "date": "Date", 8 | "type": "Type", 9 | "area": "Area", 10 | "priority": "Priority", 11 | "organizer": "Organizer", 12 | "desc": "Description", 13 | "tag": "Tag", 14 | "rate": "Rate", 15 | "create": "Create", 16 | "reset": "Reset", 17 | "holder": { 18 | "area": "Pls select area", 19 | "tag": "Pls select tag", 20 | "date": "Pls Select date", 21 | "time": "Pls Select time" 22 | }, 23 | "label": { 24 | "tag": { 25 | "st": "Ticket", 26 | "reduction": "Discount", 27 | "points": "Points" 28 | } 29 | }, 30 | "city": { 31 | "sh": "ShangHai", 32 | "bj": "BeiJing", 33 | "gz": "GuangZhou", 34 | "ly": "Lyster", 35 | "sz": "ShenZhen" 36 | }, 37 | "instDist": "JD", 38 | "price": "Discount", 39 | "rights": "Rights", 40 | "medium": "Medium", 41 | "high": "High", 42 | "rule": { 43 | "account": { 44 | "required": "Pls input account name", 45 | "length": "Length is no longer than 6" 46 | }, 47 | "region": { 48 | "required": "Pls select region" 49 | }, 50 | "date1": { 51 | "required": "Pls select date" 52 | }, 53 | "date2": { 54 | "required": "Pls select time" 55 | }, 56 | "type": { 57 | "required": "Pls select at least on type" 58 | }, 59 | "priority": { 60 | "required": "Pls select priority" 61 | }, 62 | "rate": { 63 | "required": "Pls select rate" 64 | }, 65 | "desc": { 66 | "required": "Pls input description" 67 | } 68 | }, 69 | "success": "Create successfully!", 70 | "failed": "Create failure!" 71 | }, 72 | "example": { 73 | "title1": "Button, Counter, Radio (City is a Vuex demo)", 74 | "title2": "Radio, Checkbox, Input, Multi-Select", 75 | "title3": "Cascader, Switch, Slider", 76 | "title4": "Data Form", 77 | "food": "Food", 78 | "counter": "Counter", 79 | "city": "City", 80 | "inPh": "Please input", 81 | "selPh": "Please select", 82 | "pop": "Popover" 83 | }, 84 | "nav": { 85 | "kitchenSink": "Kitchen Sink examples", 86 | "demo": "Components", 87 | "list": "Table List", 88 | "create": "Creation Form", 89 | "charts": "Charts", 90 | "about": "About" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /client/locales/examples/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "activity": { 3 | "title": { 4 | "create": "Créer une activité" 5 | }, 6 | "account": "Compte", 7 | "date": "Date", 8 | "type": "Type", 9 | "area": "Région", 10 | "priority": "Priorité", 11 | "organizer": "Organisateur", 12 | "desc": "Description", 13 | "tag": "Étiquette", 14 | "rate": "Appréciation", 15 | "create": "Créer", 16 | "reset": "Réinitialiser", 17 | "holder": { 18 | "area": "Choisir une région", 19 | "tag": "Choisir une étiquette", 20 | "date": "Choisir une date", 21 | "time": "Choisir un moment" 22 | }, 23 | "label": { 24 | "tag": { 25 | "st": "Billet", 26 | "reduction": "Rabais", 27 | "points": "Points" 28 | } 29 | }, 30 | "city": { 31 | "sh": "ShangHai", 32 | "bj": "BeiJing", 33 | "gz": "GuangZhou", 34 | "ly": "Lyster", 35 | "sz": "ShenZhen" 36 | }, 37 | "instDist": "JD", 38 | "price": "Rabais", 39 | "rights": "Droit", 40 | "medium": "Médium", 41 | "high": "Élevé", 42 | "rule": { 43 | "account": { 44 | "required": "Veuillez spécifier un nom d’utilisateur", 45 | "length": "Il y a un maximum de 6 caractères" 46 | }, 47 | "region": { 48 | "required": "Veuillez spécifier une région" 49 | }, 50 | "date1": { 51 | "required": "Veuillez spécifier une date" 52 | }, 53 | "date2": { 54 | "required": "Veuillez spécifier un moment" 55 | }, 56 | "type": { 57 | "required": "Il faut au moins avoir choisi un Type" 58 | }, 59 | "priority": { 60 | "required": "Veuillez choisir une priorité" 61 | }, 62 | "rate": { 63 | "required": "Vous devez absolument donner votre degré d’appréciation" 64 | }, 65 | "desc": { 66 | "required": "Veuillez entrer une description" 67 | } 68 | }, 69 | "success": "L’Activité crée!", 70 | "failed": "Il a été impossible de créer l’activité, désolé!" 71 | }, 72 | "example": { 73 | "title1": 74 | "Button, Counter, Radio (Le champ City est un demo d’usage de Vuex)", 75 | "title2": "Radio, Checkbox, Input, Multi-Select", 76 | "title3": "Cascader, Switch, Slider", 77 | "title4": "Formulaire de données exemple", 78 | "food": "Nourriture", 79 | "counter": "Compteur", 80 | "city": "Ville", 81 | "inPh": "Veuillez entrer", 82 | "selPh": "Veuillez choisir", 83 | "pop": "Mise en avant" 84 | }, 85 | "nav": { 86 | "kitchenSink": "Examples dans Kitchen Sink", 87 | "demo": "Components", 88 | "list": "Données tabulaires", 89 | "create": "Forumlaire de création", 90 | "charts": "Chartes et graphiques", 91 | "about": "À propos" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /client/locales/examples/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "activity": { 3 | "title": { 4 | "create": "创建活动" 5 | }, 6 | "account": "账号", 7 | "date": "活动时间", 8 | "type": "活动类型", 9 | "area": "活动区域", 10 | "priority": "优先级", 11 | "organizer": "承办方", 12 | "desc": "活动描述", 13 | "tag": "活动标签", 14 | "rate": "活动评分", 15 | "create": "立即创建", 16 | "reset": "重置", 17 | "holder": { 18 | "area": "请选择活动区域", 19 | "tag": "请选择活动标签", 20 | "date": "选择日期", 21 | "time": "选择时间" 22 | }, 23 | "label": { 24 | "tag": { 25 | "st": "赠票", 26 | "reduction": "满减", 27 | "points": "赠积分" 28 | } 29 | }, 30 | "city": { 31 | "sh": "上海", 32 | "bj": "北京", 33 | "gz": "广州", 34 | "ly": "🌴", 35 | "sz": "深圳" 36 | }, 37 | "instDist": "即时配送", 38 | "price": "price", 39 | "rights": "rights", 40 | "medium": "中", 41 | "high": "高", 42 | "rule": { 43 | "account": { 44 | "required": "请输入活动名称", 45 | "length": "长度不少于 6 个字符" 46 | }, 47 | "region": { 48 | "required": "请选择活动区域" 49 | }, 50 | "date1": { 51 | "required": "请选择日期" 52 | }, 53 | "date2": { 54 | "required": "请选择时间" 55 | }, 56 | "type": { 57 | "required": "请至少选择一个活动类型" 58 | }, 59 | "priority": { 60 | "required": "请选择活动优先级" 61 | }, 62 | "rate": { 63 | "required": "请选择活动评分" 64 | }, 65 | "desc": { 66 | "required": "请填写活动描述" 67 | } 68 | }, 69 | "success": "提交成功!", 70 | "failed": "提交失败!" 71 | }, 72 | "example": { 73 | "title1": "按钮, 计数器, 单选框 (City 为 Vuex 用法)", 74 | "title2": "单选框, 多选框, 输入框, 多选下拉框", 75 | "title3": "级联选择器, 开关, 滑块", 76 | "title4": "数据表单", 77 | "food": "食物", 78 | "counter": "计数器", 79 | "city": "城市", 80 | "inPh": "请输入内容", 81 | "selPh": "请选择", 82 | "pop": "弹框" 83 | }, 84 | "nav": { 85 | "kitchenSink": "Kitchen Sink 组件", 86 | "activity": "样例", 87 | "demo": "Element 组件", 88 | "list": "表格样例", 89 | "create": "表单样例", 90 | "charts": "图表样例", 91 | "about": "关于" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /client/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": { 3 | "userRequired": "Un nom d’utilisateur est requis", 4 | "userPlaceholder": "Nom d’utilisateur", 5 | "pwdRequired": "Un mot de passe est requis", 6 | "pwdPlaceholder": "Mot de passe", 7 | "captchaRequired": 8 | "Avoir saisi correctement le «Captcha» est requis pour se connecter", 9 | "captchaPlaceholder": "Captcha", 10 | "login": "Se connecter" 11 | }, 12 | "nav": { 13 | "home": "Accueil" 14 | }, 15 | "head": { 16 | "pwd": "Mot de passe", 17 | "exit": "Quitter" 18 | }, 19 | "tagline": 20 | "Point de départ d’une Application Web utilisant Vue.js 2.x, Koa 2.x, Element-UI, Axios, Vue i18n, et Nuxt.js" 21 | } 22 | -------------------------------------------------------------------------------- /client/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": { 3 | "userRequired": "用户名不能为空", 4 | "userPlaceholder": "请输入用户名", 5 | "pwdRequired": "密码不能为空", 6 | "pwdPlaceholder": "请输入密码", 7 | "captchaRequired": "验证码不能为空", 8 | "captchaPlaceholder": "请输入验证码", 9 | "login": "登录" 10 | }, 11 | "nav": { 12 | "home": "首页" 13 | }, 14 | "head": { 15 | "pwd": "修改密码", 16 | "exit": "退出" 17 | }, 18 | "tagline": 19 | "Application boilerplate based on Vue.js 2.x, Koa 2.x, Element-UI, Axios, Vue i18n and Nuxt.js" 20 | } 21 | -------------------------------------------------------------------------------- /client/middleware/check-auth.js: -------------------------------------------------------------------------------- 1 | export default async ({ 2 | redirect, 3 | route, 4 | store, 5 | req, 6 | $axios 7 | }) => { 8 | // If nuxt generate, pass this middleware 9 | if (process.static) { return } 10 | const maybeReq = process.server ? req : null 11 | const hasSession = maybeReq !== null && !!maybeReq.session 12 | let maybeAuthenticated = await store.getters.authenticated 13 | if (hasSession === true && maybeAuthenticated === false) { 14 | const { data } = await $axios.get('/hpi/auth/whois') 15 | store.commit('SET_USER', data) 16 | maybeAuthenticated = data.authenticated || false 17 | } 18 | const currentPath = route.path 19 | const isNotLogin = currentPath !== '/login' 20 | if (isNotLogin && maybeAuthenticated === false) { 21 | redirect('/login', { page: route.fullPath }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/pages/about.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | About Page 4 | Hello, I am the about page :) 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/pages/account/token.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | {{ title }} 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 27 | Copy 28 | 29 | validate 30 | whois 31 | token 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 69 | 70 | 81 | -------------------------------------------------------------------------------- /client/pages/examples/activity/create.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ $t(title) }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 44 | 45 | 50 | -------------------------------------------------------------------------------- /client/pages/examples/activity/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ $t('nav.list') }} 7 | 8 | 16 | 17 | 18 | 19 | 20 | {{ scope.row.date }} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {{ $t('nav.list') }} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {{ props.row.region }} 43 | 44 | 45 | {{ props.row.priority }} 46 | 47 | 48 | {{ props.row.organizer }} 49 | 50 | 51 | {{ props.row.desc }} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {{ scope.row.date }} 60 | 61 | 62 | 69 | 70 | 74 | {{ tag.row.type }} 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 109 | 110 | 136 | -------------------------------------------------------------------------------- /client/pages/examples/charts.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bar Chart 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Bar Chart 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Doughnut Chart 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Pie Chart 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Reactive Chart 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Scatter Chart 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 94 | 95 | 112 | -------------------------------------------------------------------------------- /client/pages/examples/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ $t('example.title1') }} 7 | 8 | 9 | 10 | 11 | {{ $t('example.food') }}: {{ food }} 12 | 13 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {{ $t('example.counter') }}: {{ num }} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {{ $t('example.city') }}: {{ $t(city) }} 36 | 37 | 38 | 44 | {{ $t(item.label) }} 45 | 46 | 47 | 48 | {{ $t('activity.city.sh') }} 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {{ $t('example.title2') }} 60 | 61 | 62 | 63 | 64 | 65 | 辽宁 66 | 67 | 68 | 浙江 69 | 70 | 71 | 台湾 72 | 73 | 74 | 75 | 76 | 77 | 78 | 中山区 79 | 80 | 81 | 东城区 82 | 83 | 84 | 松山区 85 | 86 | 87 | 和平区 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | Http:// 97 | 98 | 99 | .com 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | {{ $t('example.title3') }} 131 | 132 | 133 | 134 | 135 | 136 | 137 | Switch: 138 | 139 | 146 | 147 | 148 | 149 | 150 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | {{ $t('example.title4') }} 166 | 167 | {{ $t('example.pop') }} 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 244 | 245 | 276 | -------------------------------------------------------------------------------- /client/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hare 8 | {{ $t('tagline') }} 9 | 10 | 11 | 12 | 13 | 14 | 26 | 27 | 94 | -------------------------------------------------------------------------------- /client/pages/login.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{ $t('login.login') }} 31 | 32 | 33 | 34 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 129 | 130 | 165 | -------------------------------------------------------------------------------- /client/plugins/clipboard.client.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueClipboard from 'vue-clipboard2' 3 | 4 | export default () => { 5 | Vue.use(VueClipboard) 6 | } 7 | -------------------------------------------------------------------------------- /client/plugins/element-ui.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Element from 'element-ui' 3 | import enLocale from 'element-ui/lib/locale/lang/en' 4 | import zhLocale from 'element-ui/lib/locale/lang/zh-CN' 5 | 6 | // After plugin: i18n.js 7 | export default ({ store: { state } }) => { 8 | const locale = state.locale === 'en' ? enLocale : zhLocale 9 | Vue.use(Element, { locale }) 10 | } 11 | -------------------------------------------------------------------------------- /client/plugins/error-handler.client.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default () => { 4 | const _oldOnError = Vue.config.errorHandler 5 | Vue.config.errorHandler = (error, vm) => { 6 | if (typeof _oldOnError === 'function') { 7 | _oldOnError.call(this, error, vm) 8 | } 9 | // custom operation 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/plugins/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import defaultsDeep from 'lodash/defaultsDeep' 4 | import consts from '../utils/consts' 5 | 6 | export default ({ app, store, req }) => { 7 | Vue.use(VueI18n) 8 | if (process.server) { 9 | const Negotiator = require('negotiator') 10 | const negotiator = new Negotiator(req) 11 | const lang = negotiator.language(store.state.locales) 12 | store.commit('SET_LANG', lang || 'zh') 13 | } 14 | 15 | // Project specific locales 16 | let en = require('@/locales/en.json') 17 | let fr = require('@/locales/fr.json') 18 | let zh = require('@/locales/zh.json') 19 | 20 | // Add Examples locales ONLY if we need them for example/kitchen-sink work. 21 | if (consts.SHOW_EXAMPLES === true) { 22 | const examplesLocaleEn = require('@/locales/examples/en.json') 23 | const examplesLocaleFr = require('@/locales/examples/fr.json') 24 | const examplesLocaleZh = require('@/locales/examples/zh.json') 25 | en = defaultsDeep(examplesLocaleEn, en) 26 | fr = defaultsDeep(examplesLocaleFr, fr) 27 | zh = defaultsDeep(examplesLocaleZh, zh) 28 | } 29 | 30 | // Set i18n instance on app 31 | // This way we can use it in middleware and pages asyncData/fetch 32 | app.i18n = new VueI18n({ 33 | locale: store.state.locale || 'zh', 34 | fallbackLocale: 'zh', 35 | messages: { en, fr, zh } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /client/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarkdo/hare/7bd071b469e5b320195e993f0194be531d8d7700/client/static/favicon.ico -------------------------------------------------------------------------------- /client/static/vue.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarkdo/hare/7bd071b469e5b320195e993f0194be531d8d7700/client/static/vue.ico -------------------------------------------------------------------------------- /client/store/examples/activity.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | activities: [ 3 | { 4 | account: '0', 5 | date: '2018-01-01', 6 | type: 'price', 7 | region: '北京', 8 | priority: '高', 9 | organizer: '市场部', 10 | desc: 'Activity 0, as a default Vuex activity entry' 11 | } 12 | ] 13 | }) 14 | 15 | export const mutations = { 16 | SET_ACTIVITIES ( 17 | state, 18 | values 19 | ) { 20 | for (const activity of values) { 21 | state.activities.push(Object.assign({}, activity)) 22 | } 23 | } 24 | } 25 | 26 | export const actions = { 27 | add ({ commit }, activity) { 28 | const payload = [activity] 29 | commit('SET_ACTIVITIES', payload) 30 | } 31 | } 32 | 33 | export const getters = { 34 | activities (state) { 35 | return state.activities 36 | }, 37 | title (state) { 38 | return 'activity.title.create' 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/store/examples/index.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | city: 'activity.city.ly', 3 | foods: [{ 4 | value: 'Golden Paste', 5 | label: '黄金糕' 6 | }, 7 | { 8 | value: 'Double-skinned Milk', 9 | label: '双皮奶', 10 | disabled: true 11 | }, 12 | { 13 | value: 'Oyster Omelet', 14 | label: '蚵仔煎' 15 | }, 16 | { 17 | value: 'Fine Noodles', 18 | label: '龙须面' 19 | }, 20 | { 21 | value: 'Beijing Roast Duck', 22 | label: '北京烤鸭' 23 | }], 24 | cities: [{ 25 | value: 'ShangHai', 26 | label: 'activity.city.sh' 27 | }, 28 | { 29 | value: 'BeiJing', 30 | label: 'activity.city.bj', 31 | disabled: true 32 | }, 33 | { 34 | value: 'GuangZhou', 35 | label: 'activity.city.gz' 36 | }, 37 | { 38 | value: 'Lyster', 39 | label: 'activity.city.ly' 40 | }, 41 | { 42 | value: 'ShenZhen', 43 | label: 'activity.city.sz' 44 | }], 45 | labels: [{ 46 | value: 'st', 47 | label: 'activity.label.tag.st' 48 | }, 49 | { 50 | value: 'reduction', 51 | label: 'activity.label.tag.reduction' 52 | }, 53 | { 54 | value: 'points', 55 | label: 'activity.label.tag.points' 56 | }], 57 | organizers: [{ 58 | value: 'market', 59 | label: '市场部', 60 | children: [{ 61 | value: 'market', 62 | label: '交易部' 63 | }, 64 | { 65 | value: 'execution', 66 | label: '执行部' 67 | }, 68 | { 69 | value: 'promotion', 70 | label: '推广部' 71 | }] 72 | }, 73 | { 74 | value: 'operation', 75 | label: '运营部' 76 | }, 77 | { 78 | value: 'sales', 79 | label: '销售部', 80 | children: [{ 81 | value: 'regionSales', 82 | label: '大区销售', 83 | children: [{ 84 | value: 'eastSales', 85 | label: '华东销售' 86 | }, 87 | { 88 | value: 'northSales', 89 | label: '华北销售' 90 | }, 91 | { 92 | value: 'southSales', 93 | label: '华南销售' 94 | }] 95 | }, 96 | { 97 | value: 'product', 98 | label: '商品部' 99 | }, 100 | { 101 | value: 'development', 102 | label: '客户发展' 103 | }] 104 | }] 105 | }) 106 | 107 | export const getters = { 108 | city (state) { 109 | return state.city 110 | }, 111 | organizers (state) { 112 | return state.organizers 113 | }, 114 | cities (state) { 115 | return state.cities 116 | }, 117 | foods (state) { 118 | return state.foods 119 | }, 120 | labels (state) { 121 | return state.labels 122 | } 123 | } 124 | 125 | export const mutations = { 126 | SET_CITY (state, city) { 127 | state.city = city || null 128 | } 129 | 130 | } 131 | 132 | export const actions = { 133 | checkCity ({ commit }, city) { 134 | commit('SET_CITY', city) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export const strict = true 4 | 5 | export const state = () => ({ 6 | authUser: { authenticated: false }, 7 | locale: null, 8 | locales: ['en', 'fr', 'zh'], 9 | isMenuHidden: false, 10 | account: null, 11 | session: null 12 | }) 13 | 14 | export const mutations = { 15 | SET_USER ( 16 | state, 17 | authUser = null 18 | ) { 19 | let values = { authenticated: false } 20 | if (authUser !== null) { 21 | values = Object.assign(values, authUser) 22 | } 23 | for (const [ 24 | key, 25 | value 26 | ] of Object.entries(values)) { 27 | Vue.set(state.authUser, key, value) 28 | } 29 | }, 30 | SET_LANG ( 31 | state, 32 | locale 33 | ) { 34 | const normalized = locale.toLowerCase().split('-')[0] 35 | if (state.locales.includes(normalized)) { 36 | state.locale = normalized 37 | } 38 | }, 39 | TOGGLE_MENU_HIDDEN (state) { 40 | state.isMenuHidden = !state.isMenuHidden 41 | } 42 | } 43 | 44 | export const getters = { 45 | authenticated (state) { 46 | const hasAuthenticated = Reflect.has(state.authUser, 'authenticated') 47 | let authenticated = false 48 | if (hasAuthenticated) { 49 | authenticated = state.authUser.authenticated 50 | } 51 | return authenticated 52 | }, 53 | userTimeZone (state) { 54 | const hasTimeZone = Reflect.has(state.authUser, 'tz') 55 | const timeZone = 'America/New_York' // Default, in case of 56 | return hasTimeZone ? state.authUser.tz : timeZone 57 | }, 58 | userLocale (state) { 59 | const hasLocale = Reflect.has(state.authUser, 'locale') 60 | const locale = 'en-US' // Default, in case of 61 | return hasLocale ? state.authUser.locale : locale 62 | }, 63 | authUser (state) { 64 | return state.authUser 65 | }, 66 | isMenuHidden (state) { 67 | return state.isMenuHidden 68 | }, 69 | displayName (state) { 70 | const displayName = `Anonymous` // i18n? TODO 71 | const hasDisplayNameProperty = Reflect.has(state.authUser, 'displayName') 72 | return hasDisplayNameProperty ? state.authUser.displayName : displayName 73 | } 74 | } 75 | 76 | export const actions = { 77 | /** 78 | * This is run ONLY from the backend side. 79 | * 80 | * > If the action nuxtServerInit is defined in the store, Nuxt.js will call it with the context 81 | * > (only from the server-side). 82 | * > It's useful when we have some data on the server we want to give directly to the client-side. 83 | * 84 | * https://nuxtjs.org/guide/vuex-store#the-nuxtserverinit-action 85 | * https://github.com/clarkdo/hare/blob/dev/client/store/index.js 86 | * https://github.com/nuxt/docs/blob/master/en/guide/vuex-store.md 87 | */ 88 | nuxtServerInit ({ commit }, { req }) {}, 89 | async hydrateAuthUser ({ 90 | commit 91 | }) { 92 | const { data } = await this.$axios.get('/hpi/auth/whois') 93 | const user = Object.assign({}, data) 94 | commit('SET_USER', user) 95 | }, 96 | async login ({ 97 | dispatch 98 | }, { 99 | userName, 100 | password, 101 | captcha 102 | }) { 103 | try { 104 | await this.$axios.post('/hpi/auth/login', { 105 | userName, 106 | password, 107 | captcha 108 | }) 109 | await dispatch('hydrateAuthUser') 110 | } catch (error) { 111 | let message = error.message 112 | if (error.response.data) { 113 | message = error.response.data.message || message 114 | } 115 | throw new Error(message) 116 | } 117 | }, 118 | async logout ({ commit }, callback) { 119 | await this.$axios.post('/hpi/auth/logout') 120 | commit('SET_USER') 121 | callback() 122 | }, 123 | toggleMenu ({ commit }) { 124 | commit('TOGGLE_MENU_HIDDEN') 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /client/store/menu.js: -------------------------------------------------------------------------------- 1 | export const strict = true 2 | 3 | export const state = () => ({ 4 | menus: [] 5 | }) 6 | 7 | export const mutations = { 8 | SET_MENUS (state, menus) { 9 | state.menus = menus 10 | } 11 | } 12 | 13 | export const getters = { 14 | menus (state, menus) { 15 | return state.menus 16 | } 17 | } 18 | 19 | export const actions = { 20 | addAll ({ commit }, menus) { 21 | if (Array.isArray(menus) && menus.length) { 22 | commit('SET_MENUS', menus) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/utils/bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | export default new Vue() 3 | -------------------------------------------------------------------------------- /client/utils/consts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Nuxt (client) defaults constants. 3 | * 4 | */ 5 | export default Object.freeze({ 6 | SHOW_EXAMPLES: true 7 | }) 8 | -------------------------------------------------------------------------------- /client/utils/debounce.js: -------------------------------------------------------------------------------- 1 | // Look at {@link https://github.com/jashkenas/underscore} debounce method. 2 | // Returns a function, that, as long as it continues to be invoked, will not 3 | // be triggered. The function will be called after it stops being called for 4 | // N milliseconds. If `immediate` is passed, trigger the function on the 5 | // leading edge, instead of the trailing. 6 | export default function (func, wait, immediate) { 7 | let timeout, args, context, timestamp, result 8 | 9 | const later = function () { 10 | const last = new Date().getTime() - timestamp 11 | 12 | if (last < wait && last >= 0) { 13 | timeout = setTimeout(later, wait - last) 14 | } else { 15 | timeout = null 16 | if (!immediate) { 17 | result = func.apply(context, args) 18 | if (!timeout) { context = args = null } 19 | } 20 | } 21 | } 22 | 23 | return function () { 24 | context = this 25 | args = arguments 26 | timestamp = new Date().getTime() 27 | const callNow = immediate && !timeout 28 | if (!timeout) { timeout = setTimeout(later, wait) } 29 | if (callNow) { 30 | result = func.apply(context, args) 31 | context = args = null 32 | } 33 | 34 | return result 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = { 4 | srcDir: 'client/', 5 | buildDir: 'dist/client/', 6 | rootDir: './', 7 | modern: 'server', 8 | /* 9 | ** Router config 10 | */ 11 | router: { 12 | middleware: [ 13 | 'check-auth' 14 | ] 15 | }, 16 | /* 17 | ** Headers of the page 18 | */ 19 | head: { 20 | title: 'Hare 2.0', 21 | meta: [ 22 | { charset: 'utf-8' }, 23 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 24 | { hid: 'description', name: 'description', content: 'Nuxt.js project' } 25 | ], 26 | link: [ 27 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 28 | ] 29 | }, 30 | /* 31 | ** Build config 32 | */ 33 | build: { 34 | publicPath: '/hare/', 35 | extractCSS: true, 36 | babel: { 37 | plugins: [ 38 | ['@babel/plugin-proposal-decorators', { legacy: true }], 39 | ['@babel/plugin-proposal-class-properties', { loose: true }] 40 | ] 41 | }, 42 | extend (config) { 43 | config.plugins.push( 44 | new webpack.IgnorePlugin({ 45 | resourceRegExp: /^\.\/locale$/, 46 | contextRegExp: /moment$/ 47 | }) 48 | ) 49 | } 50 | }, 51 | /* 52 | ** Customize the Progress Bar 53 | */ 54 | loading: { 55 | color: '#60bbff' 56 | }, 57 | /* 58 | ** Generate config 59 | */ 60 | generate: { 61 | dir: '.generated' 62 | }, 63 | /* 64 | ** Global CSS 65 | */ 66 | css: [ 67 | 'normalize.css/normalize.css', 68 | 'element-ui/lib/theme-chalk/index.css', 69 | { src: '@/assets/styles/main.scss', lang: 'scss' } 70 | ], 71 | /* 72 | ** Add element-ui in our app, see plugins/element-ui.js file 73 | */ 74 | plugins: [ 75 | '@/plugins/i18n', 76 | '@/plugins/element-ui', 77 | '@/plugins/clipboard.client', 78 | '@/plugins/error-handler.client' 79 | ], 80 | modules: [ 81 | '@nuxtjs/axios' 82 | ], 83 | axios: { 84 | browserBaseURL: '/' 85 | }, 86 | // koa-proxies for dev, options reference https://github.com/nodejitsu/node-http-proxy#options 87 | development: { 88 | proxies: [ 89 | /* { 90 | path: '/hpi/', 91 | target: 'http://localhost:3000/', 92 | logs: true, 93 | prependPath: false, 94 | changeOrigin: true, 95 | rewrite: path => path.replace(/^\/pages(\/|\/\w+)?$/, '/service') 96 | } */ 97 | ] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hare", 3 | "version": "1.0.0-alpha.0", 4 | "description": "Based on Vue.js 2.x, Koa 2.x, Element-UI and Nuxt.js", 5 | "author": "Clark Du", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/clarkdo/hare.git" 10 | }, 11 | "scripts": { 12 | "dev": "cross-env DEBUG=nuxt:* nodemon -w server -w nuxt.config.js server/app.js", 13 | "dev:pm2": "pm2 start yarn --name=hare -- dev", 14 | "test": "yarn lint && yarn build:client && ava", 15 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 16 | "lint:fix": "eslint --fix --ext .js,.vue --ignore-path .gitignore .", 17 | "build": "yarn build:client && yarn build:server", 18 | "build:client": "nuxt build", 19 | "build:server": "rimraf dist/server && cpx \"{nuxt.config.js,server/**}\" dist", 20 | "start": "cross-env NODE_ENV=production node dist/server/app.js", 21 | "start:pm2": "pm2 start yarn --name=hare -- start", 22 | "analyze": "nuxt build --analyze", 23 | "generate": "nuxt generate" 24 | }, 25 | "husky": { 26 | "hooks": { 27 | "pre-commit": "lint-staged" 28 | } 29 | }, 30 | "lint-staged": { 31 | "{client,server,test}/**/*.{js,vue}": [ 32 | "eslint --ext .js,.vue --ignore-path .gitignore" 33 | ] 34 | }, 35 | "dependencies": { 36 | "@nuxtjs/axios": "^5.5.4", 37 | "bunyan": "^1.8.12", 38 | "chart.js": "^2.8.0", 39 | "element-ui": "2.7.2", 40 | "js-cookie": "^2.2.0", 41 | "js-yaml": "^3.13.1", 42 | "jwt-decode": "^2.2.0", 43 | "koa": "^2.7.0", 44 | "koa-body": "^4.1.0", 45 | "koa-bunyan": "^1.0.2", 46 | "koa-bunyan-logger": "^2.1.0", 47 | "koa-compress": "^3.0.0", 48 | "koa-proxies": "^0.8.1", 49 | "koa-router": "^7.4.0", 50 | "koa-session": "^5.12.2", 51 | "mkdirp": "^0.5.1", 52 | "negotiator": "^0.6.2", 53 | "normalize.css": "^8.0.1", 54 | "nuxt": "^2.8.1", 55 | "nuxt-property-decorator": "^2.3.0", 56 | "svg-captcha": "^1.4.0", 57 | "vue-chartjs": "^3.4.2", 58 | "vue-clipboard2": "^0.3.0", 59 | "vue-i18n": "^8.12.0", 60 | "xmlify": "^1.1.0" 61 | }, 62 | "devDependencies": { 63 | "@nuxtjs/eslint-config": "^1.0.1", 64 | "ava": "^2.2.0", 65 | "babel-eslint": "^10.0.2", 66 | "cpx": "^1.5.0", 67 | "cross-env": "^5.2.0", 68 | "eslint": "^6.1.0", 69 | "eslint-plugin-nuxt": "^0.4.3", 70 | "husky": "^3.0.2", 71 | "lint-staged": "^9.2.1", 72 | "lodash": "^4.17.15", 73 | "moxios": "^0.4.0", 74 | "node-sass": "^4.12.0", 75 | "nodemon": "^1.19.1", 76 | "rimraf": "^2.6.3", 77 | "sass-loader": "^7.1.0" 78 | }, 79 | "engines": { 80 | "node": ">=8.0.0 <12", 81 | "npm": ">=5.0.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs" 4 | ], 5 | "baseBranches": [ 6 | "dev" 7 | ], 8 | "lockFileMaintenance": { 9 | "enabled": true 10 | }, 11 | "ignoreDeps": [ 12 | "element-ui" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const { Nuxt, Builder } = require('nuxt') 3 | const chalk = require('chalk') 4 | const proxy = require('koa-proxies') 5 | const config = require('../nuxt.config.js') 6 | const useMiddlewares = require('./middlewares') 7 | const useRoutes = require('./routes') 8 | const consts = require('./utils/consts') 9 | 10 | // Start nuxt.js 11 | async function start () { 12 | const host = consts.HOST 13 | const port = consts.PORT 14 | const app = new Koa() 15 | 16 | app.keys = ['hare-server'] 17 | config.dev = app.env !== 'production' 18 | 19 | const nuxt = new Nuxt(config) 20 | // Build only in dev mode 21 | if (config.dev) { 22 | const devConfigs = config.development 23 | if (devConfigs && devConfigs.proxies) { 24 | for (const proxyItem of devConfigs.proxies) { 25 | // eslint-disable-next-line no-console 26 | console.log( 27 | `Active Proxy: path[${proxyItem.path}] target[${proxyItem.target}]` 28 | ) 29 | app.use(proxy(proxyItem.path, proxyItem)) 30 | } 31 | } 32 | await new Builder(nuxt).build() 33 | } 34 | 35 | // select sub-app (admin/api) according to host subdomain (could also be by analysing request.url); 36 | // separate sub-apps can be used for modularisation of a large system, for different login/access 37 | // rights for public/protected elements, and also for different functionality between api & web 38 | // pages (content negotiation, error handling, handlebars templating, etc). 39 | 40 | app.use(async (ctx, next) => { 41 | // use subdomain to determine which app to serve: www. as default, or admin. or api 42 | // note: could use root part of path instead of sub-domains e.g. ctx.request.url.split('/')[1] 43 | ctx.state.subapp = ctx.url.split('/')[1] // subdomain = part after first '/' of hostname 44 | if (ctx.state.subapp !== consts.API) { 45 | ctx.status = 200 // koa defaults to 404 when it sees that status is unset 46 | ctx.req.session = ctx.session 47 | await new Promise((resolve, reject) => { 48 | nuxt.render(ctx.req, ctx.res, err => err ? reject(err) : resolve()) 49 | }) 50 | } else { 51 | await next() 52 | } 53 | }) 54 | 55 | useMiddlewares(app) 56 | useRoutes(app) 57 | 58 | app.listen(port, host) 59 | const _host = host === '0.0.0.0' ? 'localhost' : host 60 | // eslint-disable-next-line no-console 61 | console.log('\n' + chalk.bgGreen.black(' OPEN ') + chalk.green(` http://${_host}:${port}\n`)) 62 | } 63 | 64 | start() 65 | -------------------------------------------------------------------------------- /server/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth.login.required.missing": "Required login input missing", 3 | "auth.login.captcha.invalid": "Invalid Captcha input", 4 | "auth.login.service.error": "Call OAuth service failed" 5 | } 6 | -------------------------------------------------------------------------------- /server/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth.login.required.missing": "用户名/密码未填写", 3 | "auth.login.captcha.invalid": "验证码输入错误", 4 | "auth.login.service.error": "登录失败, 具体信息请联系维护人员" 5 | } 6 | -------------------------------------------------------------------------------- /server/middlewares/content.js: -------------------------------------------------------------------------------- 1 | const xmlify = require('xmlify') // JS object to XML 2 | const yaml = require('js-yaml') // JS object to YAML 3 | 4 | module.exports = async function contentNegotiation (ctx, next) { 5 | await next() 6 | 7 | if (!ctx.body) { return } // no content to return 8 | 9 | // check Accept header for preferred response type 10 | const type = ctx.accepts('json', 'xml', 'yaml', 'text') 11 | 12 | switch (type) { 13 | case 'json': 14 | default: 15 | delete ctx.body.root // xml root element 16 | break // ... koa takes care of type 17 | case 'xml': 18 | try { 19 | const root = ctx.body.root // xml root element 20 | delete ctx.body.root 21 | ctx.body = xmlify(ctx.body, root) 22 | ctx.type = type // Only change type if xmlify did not throw 23 | } catch (e) { 24 | ctx.log.info(`Could not convert to XML, falling back to default`) 25 | } 26 | break 27 | case 'yaml': 28 | case 'text': 29 | delete ctx.body.root // xml root element 30 | ctx.type = 'yaml' 31 | ctx.body = yaml.dump(ctx.body) 32 | break 33 | case false: 34 | ctx.throw(406) // "Not acceptable" - can't furnish whatever was requested 35 | break 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/middlewares/errors.js: -------------------------------------------------------------------------------- 1 | module.exports = async function handleErrors (ctx, next) { 2 | try { 3 | await next() 4 | } catch (e) { 5 | ctx.status = e.status || 500 6 | switch (ctx.status) { 7 | case 204: // No Content 8 | break 9 | case 401: // Unauthorized 10 | case 403: // Forbidden 11 | case 404: // Not Found 12 | case 406: // Not Acceptable 13 | case 409: // Conflict 14 | ctx.body = { 15 | root: 'error' 16 | // ...e 17 | } 18 | break 19 | default: 20 | case 500: // Internal Server Error (for uncaught or programming errors) 21 | ctx.log.error(ctx.status, e.message) 22 | ctx.body = { 23 | root: 'error' 24 | // ...e 25 | } 26 | if (ctx.app.env !== 'production') { ctx.body.stack = e.stack } 27 | ctx.app.emit('error', e, ctx) // github.com/koajs/koa/wiki/Error-Handling 28 | break 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/middlewares/index.js: -------------------------------------------------------------------------------- 1 | const body = require('koa-body') // body parser 2 | const compress = require('koa-compress') // HTTP compression 3 | const session = require('koa-session') // session for flash messages 4 | 5 | const consts = require('../utils/consts') 6 | const useLogger = require('./logger') 7 | const content = require('./content') 8 | const examples = require('./errors') 9 | const responseTime = require('./response-time') 10 | const robots = require('./response-time') 11 | 12 | module.exports = (app) => { 13 | useLogger(app) 14 | // Add valid and beforeSave hooks here to ensure session is valid #TODO 15 | const SESSION_CONFIG = { 16 | key: consts.SESS_KEY 17 | } 18 | // session for flash messages (uses signed session cookies, with no server storage) 19 | app.use(session(SESSION_CONFIG, app)) 20 | app.use(responseTime) 21 | // HTTP compression 22 | app.use(compress({})) 23 | app.use(robots) 24 | // parse request body into ctx.request.body 25 | app.use(body()) 26 | app.use(content) 27 | app.use(examples) 28 | } 29 | -------------------------------------------------------------------------------- /server/middlewares/logger.js: -------------------------------------------------------------------------------- 1 | const bunyan = require('bunyan') 2 | const mkdirp = require('mkdirp') 3 | const koaBunyan = require('koa-bunyan') 4 | const koaLogger = require('koa-bunyan-logger') 5 | 6 | module.exports = function useLogger (app) { 7 | const isWin = process.platform.startsWith('win') 8 | // logging 9 | let logDir = process.env.LOG_DIR || (isWin ? 'C:\\\\log' : '/var/tmp/log') 10 | mkdirp.sync(logDir) 11 | logDir = logDir.replace(/(\\|\/)+$/, '') + (isWin ? '\\\\' : '/') 12 | 13 | const level = app.env === 'production' ? 'info' : 'debug' 14 | 15 | const access = { 16 | type: 'rotating-file', 17 | path: `${logDir}hare-access.log`, 18 | level, 19 | period: '1d', 20 | count: 4 21 | } 22 | const error = { 23 | type: 'rotating-file', 24 | path: `${logDir}hare-error.log`, 25 | level: 'error', 26 | period: '1d', 27 | count: 4 28 | } 29 | const logger = bunyan.createLogger({ 30 | name: 'hare', 31 | streams: [ 32 | access, 33 | error 34 | ] 35 | }) 36 | app.use(koaBunyan(logger, { level })) 37 | app.use(koaLogger(logger)) 38 | } 39 | -------------------------------------------------------------------------------- /server/middlewares/response-time.js: -------------------------------------------------------------------------------- 1 | module.exports = async function responseTime (ctx, next) { 2 | const t1 = Date.now() 3 | await next() 4 | const t2 = Date.now() 5 | ctx.set('X-Response-Time', Math.ceil(t2 - t1) + 'ms') 6 | 7 | /** 8 | * In case you wanna see what you received from postRequest, or other endpoints. 9 | */ 10 | const logRequestUrlResponse = '/hpi/auth/login' 11 | const logHpiAuthLogin = ctx.request.url === logRequestUrlResponse 12 | if (logHpiAuthLogin) { 13 | const debugObj = JSON.parse(JSON.stringify(ctx)) 14 | const body = JSON.parse(JSON.stringify(ctx.body || null)) 15 | const responseHeaders = JSON.parse(JSON.stringify(ctx.response.header)) 16 | const requestHeaders = JSON.parse(JSON.stringify(ctx.request.header)) 17 | ctx.log.info(`Received for ${logRequestUrlResponse}`, { ctx: debugObj, body, responseHeaders, requestHeaders }) 18 | } 19 | const isHpi = /^\/hpi\//.test(ctx.request.url) 20 | const logHpi = false 21 | if (isHpi && logHpi && logHpiAuthLogin === false) { 22 | const headers = Object.assign({}, JSON.parse(JSON.stringify(ctx.request.header))) 23 | ctx.log.info(`Request headers for ${ctx.url}`, headers) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/middlewares/robots.js: -------------------------------------------------------------------------------- 1 | module.exports = async function robots (ctx, next) { 2 | await next() 3 | // only search-index www subdomain 4 | if (ctx.hostname.slice(0, 3) !== 'www') { 5 | ctx.response.set('X-Robots-Tag', 'noindex, nofollow') 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | // TODO: support models 2 | -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 2 | /* Routes to handle authentication */ 3 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 4 | const koaRouter = require('koa-router') 5 | const svgCaptcha = require('svg-captcha') 6 | const { get } = require('lodash') 7 | const translatorFactory = require('../utils/translator') 8 | const consts = require('../utils/consts') 9 | const { 10 | decode, 11 | createRequest, 12 | getUserData 13 | } = require('../utils/helpers') 14 | 15 | /** 16 | * Have a look at ../utils/consts.js 17 | */ 18 | const ENDPOINT_BACKEND_AUTH = consts.ENDPOINT_BACKEND_AUTH 19 | const ENDPOINT_BACKEND_VALIDATE = consts.ENDPOINT_BACKEND_VALIDATE 20 | 21 | /* 22 | * Feature flag whether or not we want to mock authentication. 23 | * This should maybe in consts, or via .env file. TODO. 24 | */ 25 | const MOCK_ENDPOINT_BACKEND = consts.MOCK_ENDPOINT_BACKEND === true 26 | 27 | /** 28 | * Notice we’re not setting BASE_API as /hpi/auth 29 | * because this file is about serving readymade data for 30 | * Vue.js. 31 | * 32 | * Meaning that the client’s .vue files would call /hpi/auth/login 33 | * which this fill will answer for, BUT = require(here, we’ll call 34 | * other endpoints that aren’t available to the public. 35 | * 36 | * In other words, this Koa sub app responds to /hpi (consts.BASE_API) 37 | * and if you need mocking an actual backend, provide a mocking answser 38 | * to a canonical endpoint URL, with /hpi as a prefix. 39 | */ 40 | const router = koaRouter({ 41 | prefix: consts.BASE_API 42 | }) 43 | 44 | /** 45 | * Refer to ../utils/translator 46 | * One would want to detect user locale and serve in proper language 47 | * but more work would be needed here, because this isn’t run 48 | * client-side and with fresh data for every load, here it’s a runtime 49 | * that has a persistent state. 50 | * We therefore can’t have Koa keep in-memory ALL possible translations. 51 | */ 52 | const translator = translatorFactory('en') 53 | 54 | /** 55 | * Answer to Authentication requests = require(Vue/Nuxt. 56 | * 57 | * Notice we're also setting a cookie here. 58 | * That is because we WANT it to be HttpOnly, Nuxt (in ../client/) 59 | * can't really do that. 60 | * 61 | * Since a JWT token is authentication proof, we do not want it to be 62 | * stolen or accessible. 63 | * HttpOnly Cookie is made for this. 64 | */ 65 | router.post('/auth/login', async (ctx) => { 66 | const user = ctx.request.body 67 | if (!user || !user.userName || !user.password) { 68 | ctx.throw(401, translator.translate('auth.login.required.missing')) 69 | } 70 | let enforceCaptcha = false 71 | const shouldBe = ctx.session.captcha.toLowerCase() || 'bogus-user-captcha-entry' 72 | const userCaptchaEntry = user.captcha.toLowerCase() || 'bogus-should-be' 73 | enforceCaptcha = shouldBe !== userCaptchaEntry 74 | // enforceCaptcha = false 75 | if (enforceCaptcha) { 76 | ctx.throw(401, translator.translate('auth.login.captcha.invalid')) 77 | } 78 | try { 79 | // Assuming your API only wants base64 encoded version of the password 80 | const password = Buffer.from(user.password).toString('base64') 81 | const payload = { 82 | username: user.userName, // Maybe your username field isn't the same 83 | password, 84 | grant_type: 'password' 85 | } 86 | const headers = { 87 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 88 | } 89 | // Maybe your get token endpoint needs other headers? 90 | // headers.Authorization = 'Basic YmFzLWNsaWVudDpYMmNYeW1nWkRrRkE3RWR0' 91 | const requestConfig = { 92 | payload, 93 | headers 94 | } 95 | const response = await createRequest('POST', ENDPOINT_BACKEND_AUTH, requestConfig) 96 | const jwt = response.access_token 97 | /** 98 | * This may (or may not) use Koa Session default storage mechanism as cookies. 99 | * 100 | * It is OK to use cookie to store JWT Token ONLY IF it is shared with the 101 | * browser as an HttpOnly cookie. 102 | * Which is what Koa Session does by default. 103 | * We are doing this here instead of = require(Axios and Nuxt because Koa 104 | * can actually write HttpOnly cookies. 105 | * 106 | * Refer to koajs/session at External Session Store [1] if you want to 107 | * NOT USE coookies at all. 108 | * 109 | * [1]: https://github.com/koajs/session#external-session-stores 110 | * 111 | * rel: #JwtTokenAreTooValuableToBeNonHttpOnly 112 | */ 113 | ctx.session.jwt = jwt 114 | ctx.body = response 115 | } catch (error) { 116 | let message = translator.translate('auth.login.service.error') 117 | ctx.log.error({ error }, message) 118 | let data = null 119 | if ((data = error && error.response && error.response.data)) { 120 | message = data.message || data.errors 121 | } 122 | ctx.throw(401, message) 123 | } 124 | }) 125 | 126 | /** 127 | * #JwtTokenAreTooValuableToBeNonHttpOnly 128 | * 129 | * What keys/values do you want to expose to the UI. 130 | * Those are taken = require(an authoritative source (the JWT token) 131 | * and we might want the UI to show some data. 132 | * 133 | * Notice one thing here, although we’re reading the raw JWT token 134 | * = require(cookie, it IS HttpOnly, so JavaScript client can't access it. 135 | * 136 | * So, if you want to expose data served = require(your trusty backend, 137 | * here is your chance. 138 | * 139 | * This is how we’ll have Vue/Nuxt (the "client") read user data. 140 | * 141 | * Reason of why we’re doing this? Refer to [1] 142 | * 143 | * [1]: https://dev.to/rdegges/please-stop-using-local-storage-1i04 144 | */ 145 | router.get('/auth/whois', async (ctx) => { 146 | const body = { 147 | authenticated: false 148 | } 149 | const jwt = ctx.session.jwt || null 150 | let data = {} 151 | if (jwt !== null) { 152 | data = decode(jwt) 153 | } 154 | let userData = {} 155 | try { 156 | userData = await getUserData(jwt) 157 | const UserInfo = get(userData, 'UserInfo', {}) 158 | data.UserInfo = Object.assign({}, UserInfo) 159 | body.authenticated = userData.status === 'valid' || false 160 | } catch (e) { 161 | // Nothing to do, body.authenticated defaults to false. Which would be what we want. 162 | } 163 | 164 | /** 165 | * Each key represent the name you want to expose. 166 | * Each member has an array of two; 167 | * Index 0 is "where" inside the decoded token you want to get data from 168 | * Index 1 is the default value 169 | */ 170 | const keysMapWithLodashPathAndDefault = { 171 | userName: ['UserInfo.UserName', 'anonymous'], 172 | uid: ['userId', ''], 173 | roles: ['scope', []], 174 | exp: ['exp', 9999999999999], 175 | displayName: ['UserInfo.DisplayName', 'Anonymous'], 176 | tz: ['UserInfo.TimeZone', 'UTC'], 177 | locale: ['UserInfo.PreferredLanguage', 'en-US'] 178 | } 179 | 180 | for (const [ 181 | key, 182 | pathAndDefault 183 | ] of Object.entries(keysMapWithLodashPathAndDefault)) { 184 | const path = pathAndDefault[0] 185 | const defaultValue = pathAndDefault[1] 186 | const attempt = get(data, path, defaultValue) 187 | body[key] = attempt 188 | } 189 | 190 | ctx.status = 200 191 | ctx.body = body 192 | }) 193 | 194 | /** 195 | * This is to compensate using localStorage for token 196 | * Ideally, this should NOT be used as-is for a production Web App. 197 | * Only moment you’d want to expose token is if you have SysAdmins 198 | * who wants to poke APIs manually and needs their JWT tokens. 199 | */ 200 | router.get('/auth/token', (ctx) => { 201 | ctx.assert(ctx.session.jwt, 401, 'Requires authentication') 202 | const body = {} 203 | try { 204 | const token = ctx.session.jwt 205 | body.jwt = token 206 | } catch (e) { 207 | // Nothing to do, body.authenticated defaults to false. Which would be what we want. 208 | } 209 | 210 | ctx.status = 200 211 | ctx.body = body 212 | }) 213 | 214 | router.get('/auth/validate', async (ctx) => { 215 | const body = {} 216 | let userData = {} 217 | let authenticated = false 218 | const jwt = ctx.session.jwt || null 219 | 220 | try { 221 | userData = await getUserData(jwt) 222 | authenticated = userData.status === 'valid' || false 223 | } catch (e) { 224 | // Nothing to do, body.authenticated defaults to false. Which would be what we want. 225 | } 226 | 227 | // Maybe your endpoint returns a string here. 228 | body.authenticated = authenticated 229 | 230 | ctx.status = 200 231 | ctx.body = body 232 | }) 233 | 234 | router.post('/auth/logout', (ctx) => { 235 | ctx.assert(ctx.session.jwt, 401, 'Requires authentication') 236 | ctx.session.jwt = null 237 | ctx.status = 200 238 | }) 239 | 240 | router.get('/auth/captcha', async (ctx, next) => { 241 | await next() 242 | const width = ctx.request.query.width || 150 243 | const height = ctx.request.query.height || 36 244 | const captcha = svgCaptcha.create({ 245 | width, 246 | height, 247 | size: 4, 248 | noise: 1, 249 | fontSize: width > 760 ? 40 : 30, 250 | // background: '#e8f5ff', 251 | ignoreChars: '0oO1iIl' 252 | }) 253 | ctx.session.captcha = captcha.text 254 | ctx.type = 'image/svg+xml' 255 | ctx.body = captcha.data 256 | }) 257 | 258 | if (MOCK_ENDPOINT_BACKEND) { 259 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 260 | * Mocking responses, this is how you can emulate an actual backend. 261 | * Notice the URL below is assumed to begin by /hpi. 262 | * 263 | * When you'll use your own backend, URLs below WILL NOT have /hpi as prefix. 264 | */ 265 | 266 | router.post(ENDPOINT_BACKEND_AUTH, (ctx) => { 267 | ctx.log.debug(`Mocking a response for ${ctx.url}`) 268 | /** 269 | * The following JWT access_token contains; 270 | * See https://jwt.io/ 271 | * 272 | * ```json 273 | * { 274 | * "aud": [ 275 | * "bas" 276 | * ], 277 | * "user_name": "admin", 278 | * "scope": [ 279 | * "read" 280 | * ], 281 | * "exp": 9999999999999, 282 | * "userId": "40288b7e5bcd7733015bcd7fd7220001", 283 | * "authorities": [ 284 | * "admin" 285 | * ], 286 | * "jti": "72ec3c43-030a-41ed-abb2-b7a269506923", 287 | * "client_id": "bas-client" 288 | * } 289 | * ``` 290 | */ 291 | ctx.body = { 292 | access_token: 293 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + 294 | 'eyJhdWQiOlsiYmFzIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic' + 295 | '2NvcGUiOlsicmVhZCJdLCJleHAiOjk5OTk5OTk5OTk5OTksIn' + 296 | 'VzZXJJZCI6IjQwMjg4YjdlNWJjZDc3MzMwMTViY2Q3ZmQ3MjI' + 297 | 'wMDAxIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoi' + 298 | 'NzJlYzNjNDMtMDMwYS00MWVkLWFiYjItYjdhMjY5NTA2OTIzI' + 299 | 'iwiY2xpZW50X2lkIjoiYmFzLWNsaWVudCJ9.' + 300 | 'uwywziNetHyfSdiqcJt6XUGy4V_WYHR4K6l7OP2VB9I' 301 | } 302 | }) 303 | router.get(ENDPOINT_BACKEND_VALIDATE, (ctx) => { 304 | ctx.log.debug(`Mocking a response for ${ctx.url}`) 305 | let fakeIsValid = false 306 | // Just mimicking we only accept as a valid session the hardcoded JWT token 307 | // = require(ENDPOINT_BACKEND_AUTH above. 308 | const tokenBeginsWith = /^Token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\./.test(ctx.querystring) 309 | if (tokenBeginsWith) { 310 | fakeIsValid = true 311 | } 312 | // When API returns strings, we will handle at validate 313 | const Status = fakeIsValid ? 'valid' : 'invalid' 314 | const validated = { 315 | Status 316 | } 317 | if (fakeIsValid) { 318 | validated.UserInfo = { 319 | UserName: 'admin', 320 | DisplayName: 'Haaw D. Minh', 321 | FirstName: 'Haaw', 322 | LastName: 'D. Minh', 323 | Email: 'root@example.org', 324 | PreferredLanguage: 'zh-HK', 325 | TimeZone: 'Asia/Hong_Kong' 326 | } 327 | } 328 | ctx.status = fakeIsValid ? 200 : 401 329 | ctx.body = validated 330 | }) 331 | } /* END MOCK_ENDPOINT_BACKEND */ 332 | 333 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 334 | 335 | module.exports = router.routes() 336 | -------------------------------------------------------------------------------- /server/routes/examples.js: -------------------------------------------------------------------------------- 1 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 2 | /* Routes to define development "kitchen sink" samples */ 3 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 4 | const koaRouter = require('koa-router') 5 | const consts = require('../utils/consts') 6 | 7 | const router = koaRouter({ 8 | prefix: consts.BASE_API 9 | }) // router middleware for koa 10 | 11 | router.get('/examples/activities', (ctx) => { 12 | ctx.status = 200 13 | ctx.body = [{ 14 | account: '活动1', 15 | date: '2017-1-1', 16 | type: 'price', 17 | region: '北京', 18 | priority: '高', 19 | organizer: '市场部', 20 | desc: 'Description example of activity 1' 21 | }, 22 | { 23 | account: '活动2', 24 | date: '2017-1-2', 25 | type: 'rights', 26 | region: '北京', 27 | priority: '高', 28 | organizer: '市场部', 29 | desc: 'Description example of activity 2' 30 | }, 31 | { 32 | account: '活动3', 33 | date: '2017-1-3', 34 | type: 'price', 35 | region: '上海', 36 | priority: '高', 37 | organizer: '市场部', 38 | desc: 'Description example of activity 3' 39 | }, 40 | { 41 | account: '活动4', 42 | date: '2017-2-4', 43 | type: 'price', 44 | region: '上海', 45 | priority: '中', 46 | organizer: '运营部', 47 | desc: 'Description example of activity 4' 48 | }, 49 | { 50 | account: '活动5', 51 | date: '2017-3-5', 52 | type: 'rights', 53 | region: '大连', 54 | priority: '高', 55 | organizer: '销售部', 56 | desc: 'Description example of activity in 大连 on March 5th' 57 | }, 58 | { 59 | account: '活动6', 60 | date: '2017-4-6', 61 | type: 'price', 62 | region: '西安', 63 | priority: '高', 64 | organizer: '市场部推广部', 65 | desc: 'Description example of activity in 西安' 66 | }, 67 | { 68 | account: '活动7', 69 | date: '2017-5-7', 70 | type: 'price', 71 | region: '大连', 72 | priority: '高', 73 | organizer: '销售部华北销售', 74 | desc: 'Description example of activity in 大连' 75 | }, 76 | { 77 | account: '活动8', 78 | date: '2017-6-8', 79 | type: 'price', 80 | region: '重庆', 81 | priority: '高', 82 | organizer: '销售部华南销售', 83 | desc: 'Description example of activity in 重庆' 84 | }, 85 | { 86 | account: '活动9', 87 | date: '2017-6-9', 88 | type: 'price', 89 | region: '南京', 90 | priority: '高', 91 | organizer: '销售部华东销售', 92 | desc: 'Description example of activity in 南京' 93 | }, 94 | { 95 | account: '活动10', 96 | date: '2017-9-10', 97 | type: 'rights', 98 | region: 'New York', 99 | priority: '高', 100 | organizer: '销售部海外部', 101 | desc: 'Description example of activity in New York' 102 | }] 103 | }) 104 | 105 | module.exports = router.routes() 106 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | const consts = require('../utils/consts') 2 | const auth = require('./auth') 3 | const examples = require('./examples') 4 | const menu = require('./menu') 5 | 6 | module.exports = (app) => { 7 | app.use(auth) 8 | app.use(menu) 9 | 10 | if (consts.SHOW_EXAMPLES === true) { 11 | app.use(examples) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/routes/menu.js: -------------------------------------------------------------------------------- 1 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 2 | /* Route to handle /menu */ 3 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 4 | const koaRouter = require('koa-router') 5 | const consts = require('../utils/consts') 6 | 7 | const SHOW_EXAMPLES = consts.SHOW_EXAMPLES === true 8 | 9 | const router = koaRouter({ 10 | prefix: consts.BASE_API 11 | }) 12 | 13 | let menu = [ 14 | { 15 | id: '1', 16 | name: 'nav.home', 17 | url: '/', 18 | icon: 'el-icon-menu' 19 | }, 20 | { 21 | id: '3', 22 | name: 'nav.about', 23 | url: '/about', 24 | icon: 'el-icon-menu' 25 | } 26 | ] 27 | 28 | if (SHOW_EXAMPLES) { 29 | menu = menu.concat([ 30 | { 31 | id: '20', 32 | name: 'nav.kitchenSink', 33 | icon: 'el-icon-goods', 34 | children: [ 35 | { 36 | id: '20-1', 37 | name: 'nav.demo', 38 | url: '/examples', 39 | icon: 'el-icon-share' 40 | }, 41 | { 42 | id: '20-2', 43 | name: 'nav.list', 44 | url: '/examples/activity', 45 | icon: 'el-icon-view' 46 | }, 47 | { 48 | id: '20-3', 49 | name: 'nav.create', 50 | url: '/examples/activity/create', 51 | icon: 'el-icon-message' 52 | }, 53 | { 54 | id: '20-4', 55 | name: 'nav.charts', 56 | url: '/examples/charts', 57 | icon: 'el-icon-picture' 58 | } 59 | ] 60 | } 61 | ]) 62 | } 63 | 64 | router.get('/ui/menu', (ctx, next) => { 65 | ctx.assert(ctx.session.jwt, 401, 'Requires authentication') 66 | 67 | ctx.status = 200 68 | ctx.body = menu 69 | }) 70 | 71 | module.exports = router.routes() 72 | -------------------------------------------------------------------------------- /server/utils/consts.js: -------------------------------------------------------------------------------- 1 | const APP = 'hare' 2 | const API = 'hpi' 3 | const BASE_API = '/hpi' 4 | const SESS_KEY = 'hare:sess' 5 | const COOKIE_JWT = 'hare_jwt' 6 | const SHOW_EXAMPLES = true 7 | const AXIOS_DEFAULT_TIMEOUT = 50000 8 | const HOST = process.env.HOST || '0.0.0.0' 9 | const PORT = process.env.PORT || '3000' 10 | const LB_ADDR = process.env.LB_ADDR || `http://${HOST}:${PORT}/hpi` 11 | 12 | /** 13 | * Where to get your JWT/OAuth bearer token. 14 | * 15 | * Notice that, at the bottom, there is a Koa handler, 16 | * meaning that if you set value here as /foo/bar, it is assumed 17 | * this service will make an off-the-band request to /foo/bar 18 | * BUT ALSO allow you responding a mocking response from /hpi/foo/bar. 19 | * 20 | * To switch such behavior, you can set LB_ADDR constant. 21 | */ 22 | const ENDPOINT_BACKEND_AUTH = '/platform/uaano/oauth/token' 23 | const ENDPOINT_BACKEND_VALIDATE = '/platform/uaano/oauth/validate' 24 | // Please, reader, fix this with proper environment variable management before deploying (!) 25 | const MOCK_ENDPOINT_BACKEND = true 26 | 27 | module.exports = Object.freeze({ 28 | APP, 29 | API, 30 | BASE_API, 31 | SESS_KEY, 32 | COOKIE_JWT, 33 | SHOW_EXAMPLES, 34 | AXIOS_DEFAULT_TIMEOUT, 35 | HOST, 36 | PORT, 37 | LB_ADDR, 38 | ENDPOINT_BACKEND_AUTH, 39 | ENDPOINT_BACKEND_VALIDATE, 40 | MOCK_ENDPOINT_BACKEND 41 | }) 42 | -------------------------------------------------------------------------------- /server/utils/helpers.js: -------------------------------------------------------------------------------- 1 | const querystring = require('querystring') 2 | const axios = require('axios') 3 | const jwtDecode = require('jwt-decode') 4 | const consts = require('../utils/consts') 5 | 6 | const decode = (token) => { 7 | return token ? jwtDecode(token) : null 8 | } 9 | 10 | /** 11 | * Handle possibility where token endpoint, at exp returns seconds instead of µ seconds 12 | */ 13 | const handleTokenExp = (exp) => { 14 | let out = exp 15 | 16 | const milliseconds = new Date().getTime() 17 | // const millisecondsDigitCount = ((milliseconds).toString()).length 18 | const seconds = Math.floor(milliseconds / 1000) 19 | const secondsDigitCount = ((seconds).toString()).length 20 | 21 | const isExpressedInSeconds = ((exp).toString()).length === secondsDigitCount 22 | // const isExpressedInMilliSeconds = ((exp).toString()).length === millisecondsDigitCount 23 | 24 | // If the exp is 25 hours or less, adjust the time to miliseconds 25 | // Otherwise let's not touch it 26 | if (isExpressedInSeconds) { 27 | const durationInSeconds = Math.floor((exp - seconds)) 28 | const hours = Math.floor(durationInSeconds / 3600) 29 | if (hours < 25) { // Make 25 configurable? 30 | out *= 1000 31 | } 32 | } 33 | 34 | return out 35 | } 36 | 37 | /** 38 | * Make an async off-the-band POST request. 39 | * 40 | * Notice that LB_ADDR can be superseeded to your own backend 41 | * instead of mocking (static) endpoint. 42 | * 43 | * Differeciation factor is when you use /hpi, Koa will take care of it 44 | * and yours MUST therefore NOT start by /hpi, and Koa will be out of the way. 45 | * 46 | * All of this is done when you set your own LB_ADDR environment setup 47 | * to point to your own API. 48 | */ 49 | const createRequest = async (method, url, requestConfig) => { 50 | const { 51 | payload = null, 52 | ...restOfRequestConfig 53 | } = requestConfig 54 | const requestConfigObj = { 55 | timeout: consts.AXIOS_DEFAULT_TIMEOUT, 56 | baseURL: consts.LB_ADDR, 57 | method, 58 | url, 59 | ...restOfRequestConfig 60 | } 61 | if (payload !== null) { 62 | requestConfigObj.data = querystring.stringify(payload) 63 | } 64 | 65 | const recv = await axios.request(requestConfigObj) 66 | const data = Object.assign({}, recv.data) 67 | 68 | return Promise.resolve(data) 69 | } 70 | 71 | const getUserData = async (token) => { 72 | const userinfo = [ 73 | 'DisplayName', 74 | 'PreferredLanguage', 75 | 'TimeZone' 76 | ] 77 | const params = { 78 | Token: token, 79 | userinfo: userinfo.join(',') 80 | } 81 | 82 | /** 83 | * Would create a request like this; 84 | * 85 | * GET /platform/uaano/oauth/validate?Token=111.222.333&userinfo=PreferredLanguage,TimeZone 86 | */ 87 | const response = await createRequest('GET', consts.ENDPOINT_BACKEND_VALIDATE, { params }) 88 | 89 | const body = { 90 | status: response.Status 91 | } 92 | body.UserInfo = response.UserInfo || {} 93 | 94 | return Promise.resolve(body) 95 | } 96 | 97 | module.exports = { 98 | decode, 99 | handleTokenExp, 100 | createRequest, 101 | getUserData 102 | } 103 | -------------------------------------------------------------------------------- /server/utils/translator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Translator service. 3 | * 4 | * Use this to translate responses Koa would send. 5 | * 6 | * We do not need all translations from client, 7 | * keep things tidy here for what we really need. 8 | * 9 | * This is a starting point, maybe it should be implemented 10 | * differently but is better than locking in raw source 11 | * messages in only one locale. 12 | */ 13 | class Translator { 14 | constructor (translated) { 15 | this.translated = translated 16 | } 17 | 18 | translate (key) { 19 | const hasTranslation = Object.prototype.hasOwnProperty.call(this.translated, key) 20 | const pick = hasTranslation ? this.translated[key] : `${key}**` 21 | return pick 22 | } 23 | } 24 | 25 | module.exports = (locale) => { 26 | const fallbackLocale = 'en' 27 | let messages = {} 28 | try { 29 | // This might be reworked differently. WebPack. 30 | const attempt = require(`../locales/${locale}.json`) 31 | messages = Object.assign({}, attempt) 32 | } catch (e) { 33 | const attempt = require(`../locales/${fallbackLocale}.json`) 34 | messages = Object.assign({}, attempt) 35 | } 36 | 37 | return new Translator(messages) 38 | } 39 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import createNuxt from './helpers/create-nuxt' 3 | 4 | let nuxt = null 5 | 6 | // Init nuxt.js and create server listening on localhost:4000 7 | test.before('Init Nuxt.js', async (t) => { 8 | nuxt = createNuxt() 9 | await nuxt.listen(3000, 'localhost') 10 | }) 11 | 12 | test('Plugin', (t) => { 13 | const plugins = nuxt.options.plugins 14 | t.is(plugins[1], '@/plugins/i18n', 'i18n plugin added to config') 15 | t.is(plugins[2], '@/plugins/element-ui', 'element-ui plugin added to config') 16 | t.is(plugins[3], '@/plugins/clipboard.client', 'clipboard plugin added to config') 17 | t.is(plugins[4], '@/plugins/error-handler.client', 'error handler plugin added to config') 18 | }) 19 | 20 | test('Modules', (t) => { 21 | const modules = nuxt.options.modules 22 | t.is(modules[0], '@nuxtjs/axios', 'Axios Nuxt Module') 23 | }) 24 | 25 | test('Middleware', async (t) => { 26 | const { html, redirected } = await nuxt.renderRoute('/', { req: { headers: { 'accept-language': 'zh' } } }) 27 | t.true(html.includes(''), 'auth plugin works 1') 28 | t.true(!html.includes('前端项目模板'), 'auth plugin works 2') 29 | t.true(redirected.path === '/login', 'auth plugin works 3') 30 | t.true(redirected.status === 302, 'auth plugin works') 31 | }) 32 | 33 | // Close server and ask nuxt to stop listening to file changes 34 | test.after('Closing server and nuxt.js', async (t) => { 35 | await nuxt.close() 36 | }) 37 | -------------------------------------------------------------------------------- /test/helpers/create-nuxt.js: -------------------------------------------------------------------------------- 1 | // import { Nuxt } from 'nuxt' 2 | // import { resolve } from 'path' 3 | const { resolve } = require('path') 4 | const { Nuxt } = require('nuxt') 5 | 6 | // export default function createNuxt () { 7 | module.exports = function createNuxt () { 8 | const rootDir = resolve(__dirname, '../../') 9 | const config = require(resolve(rootDir, 'nuxt.config.js')) 10 | config.rootDir = rootDir // project folder 11 | config.dev = false // production build 12 | const nuxt = new Nuxt(config) 13 | return nuxt 14 | } 15 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import moxios from 'moxios' 3 | import createNuxt from './helpers/create-nuxt' 4 | 5 | // We keep the nuxt and server instance 6 | // So we can close them at the end of the test 7 | let nuxt = null 8 | const req = { 9 | headers: { 10 | 'accept-language': 'zh' 11 | }, 12 | session: { 13 | jwt: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + 14 | 'eyJhdWQiOlsidGF0Il0sInVzZXJfbmFtZSI6IlRlc3RlciIsI' + 15 | 'nNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNDk0MjY4ODY0LCJ1c2' + 16 | 'VySWQiOiIxIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianR' + 17 | 'pIjoiN2FkN2VjYzUtNTdmNy00MmZlLThmZmQtYjUxMTJkNTZm' + 18 | 'M2NhIiwiY2xpZW50X2lkIjoidGF0LWNsaWVudCJ9.' + 19 | 'ovWxqcBptquNR5QUBz1it2Z3Fr0OxMvWsnXHIHTcliI' 20 | } 21 | } 22 | 23 | // TODO: refactor test 24 | // Init nuxt.js and create server listening on localhost:4000 25 | test.before('Init Nuxt.js', async (t) => { 26 | // mock axios 27 | moxios.install() 28 | moxios.stubRequest('/hpi/auth/captcha', { 29 | status: 200, 30 | data: '验证码Mock' 31 | }) 32 | nuxt = createNuxt() 33 | await nuxt.listen(3000, 'localhost') 34 | }) 35 | 36 | // Example of testing only generated html 37 | test.skip('Route /', async (t) => { 38 | const { html } = await nuxt.renderRoute('/', Object.assign({}, { req })) 39 | t.true(html.includes('Application boilerplate based on Vue.js 2.x, Koa 2.x, Element-UI, Axios, Vue i18n and Nuxt.js')) 40 | }) 41 | 42 | test.skip('Route /about', async (t) => { 43 | const { html } = await nuxt.renderRoute('/about', Object.assign({}, { req })) 44 | t.true(html.includes('About Page')) 45 | }) 46 | 47 | // Close server and ask nuxt to stop listening to file changes 48 | test.after('Closing server and nuxt.js', async (t) => { 49 | moxios.uninstall() 50 | await nuxt.close() 51 | }) 52 | -------------------------------------------------------------------------------- /test/login.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import createNuxt from './helpers/create-nuxt' 3 | 4 | // We keep the nuxt and server instance 5 | // So we can close them at the end of the test 6 | let nuxt = null 7 | const headers = { 8 | 'accept-language': 'zh' 9 | } 10 | // Init Nuxt.js and create a server listening on localhost:4000 11 | test.before('Init Nuxt.js', async (t) => { 12 | nuxt = createNuxt() 13 | await nuxt.listen(3000, 'localhost') 14 | }) 15 | 16 | test('Route /login', async (t) => { 17 | const { html } = await nuxt.renderRoute('/login', { req: { session: {}, headers } }) 18 | t.true(html.includes('placeholder="请输入用户名"')) 19 | t.true(html.includes('placeholder="请输入密码"')) 20 | }) 21 | 22 | test('Route /login with locale [en]', async (t) => { 23 | const { html } = await nuxt.renderRoute('/login', { req: { session: {}, headers: { 'accept-language': 'en' } } }) 24 | t.true(html.includes('placeholder="User Name"')) 25 | t.true(html.includes('placeholder="Password"')) 26 | }) 27 | 28 | // Close server and ask nuxt to stop listening to file changes 29 | test.after('Closing server and nuxt.js', async (t) => { 30 | await nuxt.close() 31 | }) 32 | --------------------------------------------------------------------------------
15 | 16 | 17 | 18 | {{ displayName }} 19 |
23 | 24 | 25 | 26 | {{ $t("head.pwd") }} 27 |
31 | 32 | {{ $t("head.exit") }} 33 |
13 | 14 | Back to the home page 15 | 16 |
Hello, I am the about page :)
{{ $t('example.food') }}: {{ food }}
{{ $t('example.counter') }}: {{ num }}
{{ $t('example.city') }}: {{ $t(city) }}
{{ $t('tagline') }}