├── .dockerignore
├── Procfile
├── server
├── models
│ └── user.js
├── locales
│ ├── zh.json
│ └── en.json
├── middlewares
│ ├── robots.js
│ ├── errors.js
│ ├── index.js
│ ├── logger.js
│ ├── response-time.js
│ └── content.js
├── routes
│ ├── index.js
│ ├── menu.js
│ ├── examples.js
│ └── auth.js
├── utils
│ ├── translator.js
│ ├── consts.js
│ └── helpers.js
└── app.js
├── client
├── utils
│ ├── bus.js
│ ├── consts.js
│ └── debounce.js
├── static
│ ├── vue.ico
│ └── favicon.ico
├── assets
│ ├── fonts
│ │ ├── icomoon.eot
│ │ ├── icomoon.ttf
│ │ ├── icomoon.woff
│ │ ├── element-icons.eot
│ │ ├── element-icons.ttf
│ │ ├── element-icons.woff
│ │ ├── style.css
│ │ ├── element-icons.svg
│ │ └── icomoon.svg
│ ├── img
│ │ ├── login-bg.jpeg
│ │ ├── hare.svg
│ │ ├── exit.svg
│ │ ├── avatar.svg
│ │ ├── pwd.svg
│ │ ├── logo.svg
│ │ └── hare-logo.svg
│ └── styles
│ │ └── main.scss
├── pages
│ ├── about.vue
│ ├── examples
│ │ ├── activity
│ │ │ ├── create.vue
│ │ │ └── index.vue
│ │ ├── charts.vue
│ │ └── index.vue
│ ├── index.vue
│ ├── account
│ │ └── token.vue
│ └── login.vue
├── plugins
│ ├── clipboard.client.js
│ ├── error-handler.client.js
│ ├── element-ui.js
│ └── i18n.js
├── layouts
│ ├── empty.vue
│ ├── default.vue
│ └── error.vue
├── store
│ ├── menu.js
│ ├── examples
│ │ ├── activity.js
│ │ └── index.js
│ └── index.js
├── locales
│ ├── zh.json
│ ├── en.json
│ ├── fr.json
│ └── examples
│ │ ├── zh.json
│ │ ├── en.json
│ │ └── fr.json
├── components
│ ├── examples
│ │ ├── charts
│ │ │ ├── DoughnutDemo.vue
│ │ │ ├── BarDemo.vue
│ │ │ ├── PieDemo.vue
│ │ │ ├── ScatterDemo.vue
│ │ │ ├── ReactiveDemo.vue
│ │ │ └── LineDemo.vue
│ │ └── activity
│ │ │ └── NewActivity.vue
│ ├── ForkThis.vue
│ ├── Headbar.vue
│ ├── Footer.vue
│ └── Navbar.vue
└── middleware
│ └── check-auth.js
├── ava.config.js
├── .vscode
├── settings.json
├── cSpell.json
└── launch.json
├── .github
└── ISSUE_TEMPLATE.md
├── renovate.json
├── .editorconfig
├── Dockerfile.dev
├── Dockerfile
├── .eslintrc.js
├── test
├── helpers
│ └── create-nuxt.js
├── login.test.js
├── config.test.js
└── index.test.js
├── appveyor.yml
├── .circleci
└── config.yml
├── nuxt.config.js
├── package.json
├── CODE_OF_CONDUCT.md
├── README.md
├── .gitignore
├── LICENSE
└── CHANGELOG.md
/.dockerignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm run start
2 |
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | // TODO: support models
2 |
--------------------------------------------------------------------------------
/client/utils/bus.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | export default new Vue()
3 |
--------------------------------------------------------------------------------
/client/static/vue.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clarkdo/hare/HEAD/client/static/vue.ico
--------------------------------------------------------------------------------
/client/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clarkdo/hare/HEAD/client/static/favicon.ico
--------------------------------------------------------------------------------
/client/assets/fonts/icomoon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clarkdo/hare/HEAD/client/assets/fonts/icomoon.eot
--------------------------------------------------------------------------------
/client/assets/fonts/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clarkdo/hare/HEAD/client/assets/fonts/icomoon.ttf
--------------------------------------------------------------------------------
/client/assets/fonts/icomoon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clarkdo/hare/HEAD/client/assets/fonts/icomoon.woff
--------------------------------------------------------------------------------
/client/assets/img/login-bg.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clarkdo/hare/HEAD/client/assets/img/login-bg.jpeg
--------------------------------------------------------------------------------
/client/assets/fonts/element-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clarkdo/hare/HEAD/client/assets/fonts/element-icons.eot
--------------------------------------------------------------------------------
/client/assets/fonts/element-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clarkdo/hare/HEAD/client/assets/fonts/element-icons.ttf
--------------------------------------------------------------------------------
/client/assets/fonts/element-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clarkdo/hare/HEAD/client/assets/fonts/element-icons.woff
--------------------------------------------------------------------------------
/ava.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | files: ['./test/*.test.js'],
3 | tap: false,
4 | serial: true,
5 | verbose: true
6 | }
7 |
--------------------------------------------------------------------------------
/client/pages/about.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
About Page
4 |
Hello, I am the about page :)
5 |
6 |
7 |
--------------------------------------------------------------------------------
/client/utils/consts.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Nuxt (client) defaults constants.
3 | *
4 | */
5 | export default Object.freeze({
6 | SHOW_EXAMPLES: true
7 | })
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/server/locales/zh.json:
--------------------------------------------------------------------------------
1 | {
2 | "auth.login.required.missing": "用户名/密码未填写",
3 | "auth.login.captcha.invalid": "验证码输入错误",
4 | "auth.login.service.error": "登录失败, 具体信息请联系维护人员"
5 | }
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/layouts/empty.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/assets/img/hare.svg:
--------------------------------------------------------------------------------
1 | 资源 1
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/components/examples/charts/DoughnutDemo.vue:
--------------------------------------------------------------------------------
1 |
27 |
--------------------------------------------------------------------------------
/client/components/examples/charts/BarDemo.vue:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/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/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/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/components/examples/charts/PieDemo.vue:
--------------------------------------------------------------------------------
1 |
36 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/pages/examples/activity/create.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ $t(title) }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
44 |
45 |
50 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/components/examples/charts/ScatterDemo.vue:
--------------------------------------------------------------------------------
1 |
62 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/components/examples/charts/ReactiveDemo.vue:
--------------------------------------------------------------------------------
1 |
52 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
33 |
34 |
58 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/layouts/error.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ error.statusCode }}
6 |
7 |
8 |
9 | {{ error.message }}
10 |
11 |
12 |
13 |
14 | Back to the home page
15 |
16 |
17 |
18 |
19 |
20 |
21 |
39 |
40 |
82 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Hare
8 |
{{ $t('tagline') }}
9 |
10 |
11 |
12 |
13 |
14 |
26 |
27 |
94 |
--------------------------------------------------------------------------------
/client/components/ForkThis.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
49 |
--------------------------------------------------------------------------------
/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/pages/account/token.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | {{ title }}
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
27 | Copy
28 |
29 | validate
30 | whois
31 | token
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
69 |
70 |
81 |
--------------------------------------------------------------------------------
/client/components/examples/charts/LineDemo.vue:
--------------------------------------------------------------------------------
1 |
72 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/pwd.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 密码
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/components/Headbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
37 |
38 |
39 |
40 |
64 |
65 |
102 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/client/pages/examples/charts.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Bar Chart
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Bar Chart
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Doughnut Chart
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Pie Chart
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Reactive Chart
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Scatter Chart
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
94 |
95 |
112 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/components/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
30 |
31 |
32 |
41 |
42 |
137 |
--------------------------------------------------------------------------------
/client/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
43 |
44 |
45 |
81 |
82 |
127 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  Application boilerplate based on Vue.js 2.x, Koa 2.x, Element-UI and Nuxt.js
2 |
3 | [](https://circleci.com/gh/clarkdo/hare)
4 | [](https://ci.appveyor.com/project/clarkdo/hare)
5 | [](https://snyk.io/test/github/clarkdo/hare)
6 | [](https://standardjs.com)
7 | [](https://github.com/eslint/eslint)
8 | [](https://github.com/clarkdo/hare/issues)
9 | [](https://github.com/clarkdo/hare/stargazers)
10 | [](https://raw.githubusercontent.com/clarkdo/hare/master/LICENSE)
11 |
12 | ## Installation
13 |
14 | ``` bash
15 | $ git clone git@github.com:clarkdo/hare.git
16 | $ cd hare
17 | # install dependencies
18 | $ yarn
19 | ```
20 |
21 | ## Usage
22 |
23 | ### Development
24 |
25 | ``` bash
26 | # serve with hot reloading at localhost:3000
27 | $ yarn dev
28 | ```
29 |
30 | Go to [http://localhost:3000](http://localhost:3000)
31 |
32 | ### Testing
33 |
34 | ``` bash
35 | # configure ESLint as a tool to keep codes clean
36 | $ yarn lint
37 | # use ava as testing framework, mixed with jsdom
38 | $ yarn test
39 | ```
40 |
41 | ### Production
42 |
43 | ``` bash
44 | # build for production and launch the server
45 | $ yarn build
46 | $ yarn start
47 | ```
48 |
49 | ### Generate
50 |
51 | ``` bash
52 | # generate a static project
53 | $ yarn generate
54 | ```
55 |
56 | ### Analyze
57 |
58 | ``` bash
59 | # build and launch the bundle analyze
60 | $ yarn analyze
61 | ```
62 |
63 | ### Use PM
64 |
65 | #### Further more features refer: [PM2](https://github.com/Unitech/pm2)
66 |
67 | ``` bash
68 | # install pm2 globally
69 | $ yarn global add pm2
70 | # launch development server
71 | $ yarn dev:pm2
72 | # launch production server
73 | $ yarn start:pm2
74 | # Display all processes status
75 | $ pm2 ls
76 | # Show all information about app
77 | $ pm2 show hare
78 | # Display memory and cpu usage of each app
79 | $ pm2 monit
80 | # Display logs
81 | $ pm2 logs
82 | # Stop
83 | $ pm2 stop hare
84 | # Kill and delete
85 | $ pm2 delete hare
86 | ```
87 |
88 | ### Docker Dev
89 |
90 | ``` bash
91 | # build image
92 | $ docker build -t hare-dev -f Dockerfile.dev ./
93 | $ docker run -d -p 8888:3000 -e HOST=0.0.0.0 hare-dev
94 | ```
95 |
96 | Go to [http://localhost:8888](http://locdoalhost:8888)
97 |
98 | ### Docker Production
99 |
100 | ``` bash
101 | # build bundle
102 | $ export NODE_ENV=''
103 | $ yarn
104 | $ yarn build
105 | # install production dependencies (remove devDependencies)
106 | $ yarn --prod
107 | # build image
108 | $ docker build -t hare-prod -f Dockerfile ./
109 | $ docker run -d -p 8889:3000 -e HOST=0.0.0.0 hare-prod
110 | ```
111 |
112 | Go to [http://localhost:8889](http://locdoalhost:8889)
113 |
114 | ## Documentation
115 |
116 | Vue.js documentation: [https://vuejs.org/](https://vuejs.org/)
117 |
118 | Nuxt.js documentation: [https://nuxtjs.org](https://nuxtjs.org)
119 |
120 | Element-UI documentation: [http://element.eleme.io](http://element.eleme.io/#/en-US)
121 |
122 | Koa documentation: [https://github.com/koajs/koa](https://github.com/koajs/koa)
123 |
--------------------------------------------------------------------------------
/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/assets/fonts/icomoon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Generated by IcoMoon
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/client/pages/examples/activity/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ $t('nav.list') }}
7 |
8 |
16 |
17 |
18 |
19 |
20 | {{ scope.row.date }}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {{ $t('nav.list') }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {{ props.row.region }}
43 |
44 |
45 | {{ props.row.priority }}
46 |
47 |
48 | {{ props.row.organizer }}
49 |
50 |
51 | {{ props.row.desc }}
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | {{ scope.row.date }}
60 |
61 |
62 |
69 |
70 |
74 | {{ tag.row.type }}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
109 |
110 |
136 |
--------------------------------------------------------------------------------
/client/pages/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ $t('login.login') }}
31 |
32 |
33 |
34 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
129 |
130 |
165 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/components/examples/activity/NewActivity.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | -
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | {{ $t('activity.create') }}
106 |
107 |
108 | {{ $t('activity.reset') }}
109 |
110 |
111 |
112 |
113 |
114 |
115 |
216 |
217 |
231 |
--------------------------------------------------------------------------------
/client/pages/examples/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ $t('example.title1') }}
7 |
8 |
9 |
10 |
11 | {{ $t('example.food') }}: {{ food }}
12 |
13 |
14 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{ $t('example.counter') }}: {{ num }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{ $t('example.city') }}: {{ $t(city) }}
36 |
37 |
38 |
44 | {{ $t(item.label) }}
45 |
46 |
47 |
48 | {{ $t('activity.city.sh') }}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | {{ $t('example.title2') }}
60 |
61 |
62 |
63 |
64 |
65 | 辽宁
66 |
67 |
68 | 浙江
69 |
70 |
71 | 台湾
72 |
73 |
74 |
75 |
76 |
77 |
78 | 中山区
79 |
80 |
81 | 东城区
82 |
83 |
84 | 松山区
85 |
86 |
87 | 和平区
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | Http://
97 |
98 |
99 | .com
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | {{ $t('example.title3') }}
131 |
132 |
133 |
134 |
135 |
136 |
137 | Switch:
138 |
139 |
146 |
147 |
148 |
149 |
150 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | {{ $t('example.title4') }}
166 |
167 | {{ $t('example.pop') }}
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
244 |
245 |
276 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/assets/img/hare-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ]>
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
32 |
33 |
34 |
35 | Hare
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
189 |
263 |
337 |
411 |
485 |
559 |
571 |
572 |
573 |
--------------------------------------------------------------------------------