├── .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 | # ![Logo](http://clarkdo.github.io/public/img/hare-logo-blue.svg) Application boilerplate based on Vue.js 2.x, Koa 2.x, Element-UI and Nuxt.js 2 | 3 | [![CircleCI](https://circleci.com/gh/clarkdo/hare.svg?style=svg)](https://circleci.com/gh/clarkdo/hare) 4 | [![Windows](https://ci.appveyor.com/api/projects/status/16eua6xnlnwxqomp?svg=true)](https://ci.appveyor.com/project/clarkdo/hare) 5 | [![Vulnerabilities](https://snyk.io/test/github/clarkdo/hare/badge.svg)](https://snyk.io/test/github/clarkdo/hare) 6 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 7 | [![ESLint](https://img.shields.io/badge/styled_with-eslint-blue.svg?colorB=8080f2)](https://github.com/eslint/eslint) 8 | [![Issues](https://img.shields.io/github/issues/clarkdo/hare.svg)](https://github.com/clarkdo/hare/issues) 9 | [![Stars](https://img.shields.io/github/stars/clarkdo/hare.svg)](https://github.com/clarkdo/hare/stargazers) 10 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](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 | 116 | 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 188 | 189 | 190 | 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 262 | 263 | 264 | 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 336 | 337 | 338 | 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 410 | 411 | 412 | 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 484 | 485 | 486 | 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 558 | 559 | 560 | Q/vGNwPE4wjCApV9H4QJKiKhQn1sWeEfDAl/k206sMNaBR3t2KNa6ptBKi8GYJHKIBfFJn8lI574 561 | hxD+OVXyt3fTyvgT/jNB7HYLkvpVFIPe+xyLRV9RTAGSwyHndlSIlEkxl0ElmXyOIwgYrMOoUizY 562 | wiKKN4BW9DDc/ohqMEqUA2Ea12OawBbjObjpGDaVzQhY384iIgtrZXAB3hrqKDv4GA9TMwZQoKyi 563 | ledYeA4ASlAMhW5hz2CxR1EzMnkxhwo5vPOU6Aj4HngNe5DkBUhNDR2C7wIKm4DKtsCjc3uR4QRU 564 | wmc4PmyF529+tJf5rQ16NhiGHTEC4Rg+gvGNZdiuEwTP8IlMEEzD/DwQrmFeFAjb/jhmvi/E+Asj 565 | xu/V+KcQqj8E53dKs2w4vaMOUSLNnr9RoBKf4vNQEiYFAHgazqHoNqWwgQtZjLJKIdpOXA5vp2wu 566 | Yyrhszwyz+llLDyUz+PGiEIhIyp6AG6eTbE8g3UkZRRKSdkwXqWwgQuzODxPfReUKDF9avtkCRqG 567 | 0poW+gfH2zCV4Vnpzaszt0Dtd3J695tHOtlgE1bmjPjUV4t9Y5RG6Y041bcUU2NVxzRypbivFvvH 568 | LK1FA271LaXU2PVHYuu0KNAIjAJlexRoBEeBsj0KNIKjQNkWBRrBUeAPc3r3ewm0Wj2VXuXLsdTr 569 | Qyb+OpF+lMPSYDCcSlN5BB6FX8fyZDocywBMw59gCXxJe2F1tXpSC/3w/wHQ6lQb 570 | 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 | 31 | 32 | 41 | 42 | 137 | -------------------------------------------------------------------------------- /client/components/ForkThis.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 49 | -------------------------------------------------------------------------------- /client/components/Headbar.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 64 | 65 | 102 | -------------------------------------------------------------------------------- /client/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 81 | 82 | 127 | -------------------------------------------------------------------------------- /client/components/examples/activity/NewActivity.vue: -------------------------------------------------------------------------------- 1 | 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 | 14 | 15 | 33 | 34 | 58 | -------------------------------------------------------------------------------- /client/layouts/empty.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /client/layouts/error.vue: -------------------------------------------------------------------------------- 1 | 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 | 7 | -------------------------------------------------------------------------------- /client/pages/account/token.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 69 | 70 | 81 | -------------------------------------------------------------------------------- /client/pages/examples/activity/create.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 44 | 45 | 50 | -------------------------------------------------------------------------------- /client/pages/examples/activity/index.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 109 | 110 | 136 | -------------------------------------------------------------------------------- /client/pages/examples/charts.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 94 | 95 | 112 | -------------------------------------------------------------------------------- /client/pages/examples/index.vue: -------------------------------------------------------------------------------- 1 | 178 | 179 | 244 | 245 | 276 | -------------------------------------------------------------------------------- /client/pages/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | 27 | 94 | -------------------------------------------------------------------------------- /client/pages/login.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------