├── .env
├── .env.development
├── .github
└── workflows
│ ├── deploy.yml
│ └── tests.yml
├── .gitignore
├── CHANGELOG.md
├── README.md
├── babel.config.js
├── mock.config.js
├── package.json
├── public
├── favicon.ico
└── index.html
├── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ ├── BaseFooter.vue
│ ├── BaseRouterView.vue
│ ├── BaseToast.vue
│ ├── RAdminNav.vue
│ ├── RAdminNavDrawer.vue
│ └── RAdminNavRoot.vue
├── constants.ts
├── effects
│ ├── index.ts
│ └── initiator.ts
├── global.d.ts
├── layouts
│ ├── RAdmin.vue
│ ├── RFooter.vue
│ ├── RView.module.sass
│ └── RView.ts
├── main.ts
├── plugins
│ ├── ability.ts
│ ├── persisted-state.ts
│ ├── store.ts
│ └── vuetify.ts
├── router
│ ├── guards.ts
│ ├── index.ts
│ ├── private-routes.ts
│ └── public-routes.ts
├── shared
│ └── utils.ts
├── shims-tsx.d.ts
├── shims-vue.d.ts
├── store
│ ├── global.ts
│ ├── index.ts
│ └── modules
│ │ ├── history.ts
│ │ ├── index.ts
│ │ └── user.ts
├── styles
│ └── global.sass
└── views
│ ├── Error.vue
│ ├── Home.vue
│ ├── Login.vue
│ ├── Sample.vue
│ └── User.vue
├── tests
└── unit
│ └── store
│ └── history.spec.ts
├── third-parties.js
├── tsconfig.json
├── vue.config.js
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | VUE_APP_REQUEST_BASE_URL=/
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | VUE_APP_REQUEST_BASE_URL=/
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deployment
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build-and-deploy:
10 | name: Build on node ${{ matrix.node_version }} and ${{ matrix.os }}
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | matrix:
14 | node_version: [10.X]
15 | os: [ubuntu-latest]
16 |
17 | steps:
18 | - uses: actions/checkout@v1
19 |
20 | - name: Use Node.js ${{ matrix.node_version }}
21 | uses: actions/setup-node@v1
22 | with:
23 | node-version: ${{ matrix.node_version }}
24 |
25 | - name: Use Yarn
26 | run: |
27 | npm i yarn -g
28 | yarn -v
29 |
30 | - name: Install Dependencies
31 | run: yarn
32 |
33 | - name: Build web site
34 | run: yarn build
35 |
36 | - name: Deploy output folder
37 | env:
38 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
39 | BUILD_DIR: dist
40 | TARGET_BRANCH: gh-pages
41 | run: |
42 | REMOTE_ORIGIN=https://${ACCESS_TOKEN}@github.com/${GITHUB_REPOSITORY}
43 | cd ${BUILD_DIR}
44 | git init
45 | git config user.name ${GITHUB_ACTOR}
46 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
47 | git add . -f
48 | git commit -m "deploy: from Github action - $(date +"%T")"
49 | git push --force ${REMOTE_ORIGIN} master:${TARGET_BRANCH}
50 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | test:
7 | name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }}
8 | runs-on: ${{ matrix.os }}
9 | strategy:
10 | matrix:
11 | node_version: [10.X]
12 | os: [ubuntu-latest]
13 |
14 | steps:
15 | - uses: actions/checkout@v1
16 |
17 | - name: Use Node.js ${{ matrix.node_version }}
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: ${{ matrix.node_version }}
21 |
22 | - name: Use Yarn
23 | run: |
24 | npm i yarn -g
25 | yarn -v
26 |
27 | - name: Install Dependencies
28 | run: yarn
29 |
30 | - name: Run all tests
31 | run: yarn run test:unit
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 | .todo
23 |
24 | release.sh
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [3.2.0](https://github.com/lbwa/vue-auth-boilerplate/compare/v3.1.0...v3.2.0) (2020-05-07)
6 |
7 |
8 | ### Features
9 |
10 | * **build:** optimization should only works with production build ([ba50cd3](https://github.com/lbwa/vue-auth-boilerplate/commit/ba50cd35f1d4a29fbb93ae43e27b0c2669d19265))
11 | * **html:** inject build time and commit hash ([fa2484f](https://github.com/lbwa/vue-auth-boilerplate/commit/fa2484f8d127c1a15634bb18eec8b55ea52cb7f2))
12 | * **runtime:** extract webpack runtime code ([77efeed](https://github.com/lbwa/vue-auth-boilerplate/commit/77efeed3bba93d12da24d4cd5371d5d0c5516daa))
13 | * **runtime:** inline webpack runtime code ([203ea5c](https://github.com/lbwa/vue-auth-boilerplate/commit/203ea5c868baaa09438b5e62932babeb55e7b6b0))
14 |
15 | ## [3.1.0](https://github.com/lbwa/vue-auth-boilerplate/compare/v3.0.0...v3.1.0) (2020-05-06)
16 |
17 |
18 | ### Features
19 |
20 | * **cdn:** support thrid-party JS deps served by CDN ([70c88be](https://github.com/lbwa/vue-auth-boilerplate/commit/70c88bea998b3116ac3fbeec2fda6656780fd184))
21 | * **css-cdn:** support cdn css files injection ([4a2fbf9](https://github.com/lbwa/vue-auth-boilerplate/commit/4a2fbf9fa7041667f219c7bd057026e7dc8e70bd))
22 | * **html:** support appVersion injection ([1743306](https://github.com/lbwa/vue-auth-boilerplate/commit/1743306212fa5795b70350eecb27545aa4da0fa3))
23 |
24 |
25 | ### Bug Fixes
26 |
27 | * **html:** cdn-template works with development building ([957c629](https://github.com/lbwa/vue-auth-boilerplate/commit/957c629c27262dfee14d8fdb81849356549e14ba))
28 | * **vue.config:** should work without git log, close [#23](https://github.com/lbwa/vue-auth-boilerplate/issues/23) ([faf6c8c](https://github.com/lbwa/vue-auth-boilerplate/commit/faf6c8cf0dd5c5579ea3982207908fa8f51f50c8))
29 |
30 | ## 3.0.0 (2020-04-06)
31 |
32 |
33 | ### Features
34 |
35 | * add .env files ([e8ee611](https://github.com/lbwa/vue-auth-boilerplate/commit/e8ee611575b0d259dfd61c6cc5c763deb766f5a1))
36 | * add analysis-doughnut tooltips ([aa1e08c](https://github.com/lbwa/vue-auth-boilerplate/commit/aa1e08ce920817fca0beb10686b01837cca0b24a))
37 | * add animation for transition ([6cb2505](https://github.com/lbwa/vue-auth-boilerplate/commit/6cb2505f9b60e1fa2de354ebdb970ef60699d385))
38 | * add app-footer ([cd5e29d](https://github.com/lbwa/vue-auth-boilerplate/commit/cd5e29d053c9179f09a2c6c5dcd877d21c0e09d5))
39 | * add basic request module ([6b8b78a](https://github.com/lbwa/vue-auth-boilerplate/commit/6b8b78a07586dc3ac3244046b339fcca31850991))
40 | * add chart-bar components ([a9c200a](https://github.com/lbwa/vue-auth-boilerplate/commit/a9c200aee04920ea8f5fa26deb2115f68b43d73b))
41 | * add chart-bar to analysis page ([3af3224](https://github.com/lbwa/vue-auth-boilerplate/commit/3af3224ea0b37eaafe40ff6b3ea790cdae2e8fb6))
42 | * add ChartLine to analysis page ([3512cfe](https://github.com/lbwa/vue-auth-boilerplate/commit/3512cfea45cd3f0158799d58bab7d7b18552ed3d))
43 | * add error handler page ([3d75699](https://github.com/lbwa/vue-auth-boilerplate/commit/3d756999dd9cfe43a47ce076dc50cfe14b8c26a7))
44 | * add footer link ([b3b977e](https://github.com/lbwa/vue-auth-boilerplate/commit/b3b977e2ee5d55c22b660f9248a364fa96b22258))
45 | * add interaction for login page ([9a498bf](https://github.com/lbwa/vue-auth-boilerplate/commit/9a498bf30e7963207f77c8b0236e6efe4a2acd1d))
46 | * add loading animation in the login page ([5fd9472](https://github.com/lbwa/vue-auth-boilerplate/commit/5fd94727362e06edfbf109a6a9b748e70f904d1f))
47 | * add login redirect logic ([47b64ee](https://github.com/lbwa/vue-auth-boilerplate/commit/47b64ee6f1995bc4f59220a36846d654049d26bd))
48 | * add mock data ([221d188](https://github.com/lbwa/vue-auth-boilerplate/commit/221d1887015a9ddc310452181f6ad7a155e11ec6))
49 | * add progress bar ([1d57857](https://github.com/lbwa/vue-auth-boilerplate/commit/1d5785721b221463655aba600631a4d0d0ee7583))
50 | * add rank component ([e055a05](https://github.com/lbwa/vue-auth-boilerplate/commit/e055a057026289d14b3fd1c57a291a0e14e1ee1c))
51 | * add README ([f8352e5](https://github.com/lbwa/vue-auth-boilerplate/commit/f8352e579767995accbcb3d07a066e05a4d79595))
52 | * add request API for production mode ([317e529](https://github.com/lbwa/vue-auth-boilerplate/commit/317e52972244cf2419f87d7a48ce63f5db2963f6))
53 | * add Sample page ([e0d7bf3](https://github.com/lbwa/vue-auth-boilerplate/commit/e0d7bf3eb72fdbcc89d1aecb1327aa6b3bf7006f))
54 | * add searchData process ([c7fdcf5](https://github.com/lbwa/vue-auth-boilerplate/commit/c7fdcf59ac49928a059e944f2610b77ded44962a))
55 | * add storage for analysis page ([b65cfbf](https://github.com/lbwa/vue-auth-boilerplate/commit/b65cfbfc9e09515bcfd735b3a14aafee273c3b3e))
56 | * add tooltip for app header ([a3193d6](https://github.com/lbwa/vue-auth-boilerplate/commit/a3193d6388d6fec050ee7a710001ef49c60aa321))
57 | * add User Error Login views ([2bd679b](https://github.com/lbwa/vue-auth-boilerplate/commit/2bd679b2964f09d66a76d1c22e9c9ea37adb9026))
58 | * add vuex ([f6a1f1d](https://github.com/lbwa/vue-auth-boilerplate/commit/f6a1f1df61ba6b021532af1bba2c219a0cfe44f8))
59 | * adjust form/* routes to nested routes, rather than first-level ([5de8594](https://github.com/lbwa/vue-auth-boilerplate/commit/5de8594a24e87dd409ab8b8d55ed048f3d38056e))
60 | * adjust set login/SET_ROLE strategy ([e667d06](https://github.com/lbwa/vue-auth-boilerplate/commit/e667d069b4c137511a24ec073ea565aef494565e))
61 | * All form pages basic layout ([58500fd](https://github.com/lbwa/vue-auth-boilerplate/commit/58500fd082ae485bbf6cb952b83c20daaf706ec1))
62 | * all submitting logic for step-form ([888c45b](https://github.com/lbwa/vue-auth-boilerplate/commit/888c45b0e09e6289221d9173f8dd34f957c59f03))
63 | * analysis-doughnut data stream base logic ([1ed9a52](https://github.com/lbwa/vue-auth-boilerplate/commit/1ed9a5224c0f8be66a9c556b9df809671efbdd5e))
64 | * analysis-sales-class base layout ([d3824a1](https://github.com/lbwa/vue-auth-boilerplate/commit/d3824a10c70e8d487f9c05fa03ce023b74e2e817))
65 | * analysis-sales-type baisic layout ([823d1ec](https://github.com/lbwa/vue-auth-boilerplate/commit/823d1ecc76241045552ed6e5cdc98890c7fcfae6))
66 | * ananlysis-middle chart ([2970904](https://github.com/lbwa/vue-auth-boilerplate/commit/2970904c87cda97c0d7875eb3a26ae3d62c869ae))
67 | * AppHeader components ([8e389b5](https://github.com/lbwa/vue-auth-boilerplate/commit/8e389b5533d8a82c3a047ed4f66c4ba352204c39))
68 | * bacis layout for workspace projects ([439a736](https://github.com/lbwa/vue-auth-boilerplate/commit/439a7362f8fd9a3d9082c953a74cffc6f941ca92))
69 | * base routes in dashboard ([cc1bedb](https://github.com/lbwa/vue-auth-boilerplate/commit/cc1bedb73f6413e306426f1dd7e7af9b29d21726))
70 | * basic card style in analysis page ([a6d8442](https://github.com/lbwa/vue-auth-boilerplate/commit/a6d84425a32582861a83acfba41cad942a5f4b9f))
71 | * basic current user logic in workspace page ([e4e9ee8](https://github.com/lbwa/vue-auth-boilerplate/commit/e4e9ee892809731249b2b6b889847a9111ba236b))
72 | * basic layout for header user-info ([944f985](https://github.com/lbwa/vue-auth-boilerplate/commit/944f9857a500ce5f68f8da79130c3faddad503d0))
73 | * basic layout for workspace team component ([c3877af](https://github.com/lbwa/vue-auth-boilerplate/commit/c3877af4565ddcc634f98531f434488a4e13593c))
74 | * basic layout form step-confirm & step-success ([762b1e1](https://github.com/lbwa/vue-auth-boilerplate/commit/762b1e1f73c6e6227337443982b83f05d637efa6))
75 | * basic layout in workspace activities ([1a8067c](https://github.com/lbwa/vue-auth-boilerplate/commit/1a8067cfb8c0221bdf09b12f89ca49b5c656df16))
76 | * basic layout of analysis table ([ee12499](https://github.com/lbwa/vue-auth-boilerplate/commit/ee12499f8eae854701412adc2aeada249ffbbcb9))
77 | * basic logic for basic-form ([954d95b](https://github.com/lbwa/vue-auth-boilerplate/commit/954d95b6f4e472ad34af6f1b46ce48256a42dbc4))
78 | * basic logic for i18n ([a33785a](https://github.com/lbwa/vue-auth-boilerplate/commit/a33785a5005a53d3a4ba28c023099bcda3d14d1a))
79 | * basic logic for radar request module ([9d4b43b](https://github.com/lbwa/vue-auth-boilerplate/commit/9d4b43b03fffa557176abc4096082d32f108ea0f))
80 | * basic logic for routes filter ([f6ca100](https://github.com/lbwa/vue-auth-boilerplate/commit/f6ca100c91399134fc456f6cecbb329f66469bd4))
81 | * basic logic for workspace fetching ([99157be](https://github.com/lbwa/vue-auth-boilerplate/commit/99157be410f749dca7f2e9d0a2028b4f9de8f841))
82 | * basic module vuex.store for form-step ([46d262c](https://github.com/lbwa/vue-auth-boilerplate/commit/46d262c0225120baa1e5de54c94ef5e46dbd92b4))
83 | * basic render logic in aside components ([13e8bbd](https://github.com/lbwa/vue-auth-boilerplate/commit/13e8bbd3e24b913ba2c6b3aad284341038b0c7f2))
84 | * basic-form submitting logic ([52aacc8](https://github.com/lbwa/vue-auth-boilerplate/commit/52aacc819b05e394368f5d04d6c197466c6ba14f))
85 | * change layout due to correct routes ([89bde6c](https://github.com/lbwa/vue-auth-boilerplate/commit/89bde6c0bfb23327616771bf087d46f5c2b14d2b))
86 | * chart layout in analysis tab ([7300d95](https://github.com/lbwa/vue-auth-boilerplate/commit/7300d95f31541f13f28f822364f8c0d4e00e4fd1))
87 | * color arrow icon ([b4465f8](https://github.com/lbwa/vue-auth-boilerplate/commit/b4465f829dd084da94beec144acef711a4e16ab6))
88 | * comlete all step-form ([e18ab57](https://github.com/lbwa/vue-auth-boilerplate/commit/e18ab57297382de1aabd5603471884d9229692c1))
89 | * complate request module for workspace activities ([6d3d9cc](https://github.com/lbwa/vue-auth-boilerplate/commit/6d3d9cc99f3de6939fc7d2469059d1e803f3395a))
90 | * complete analysis card ([79ca963](https://github.com/lbwa/vue-auth-boilerplate/commit/79ca9634b1a2ec473d832242075d3010bf251456))
91 | * complete analysis tab ([aeab3cc](https://github.com/lbwa/vue-auth-boilerplate/commit/aeab3ccc13a2b1f04b91c4a02dd6cc75609ecc10))
92 | * complete analysis-sales-type section ([cba4b6b](https://github.com/lbwa/vue-auth-boilerplate/commit/cba4b6b15bd6f0bfc2ba5b586a727990a298570d))
93 | * complete footer layout and style ([e9d2a98](https://github.com/lbwa/vue-auth-boilerplate/commit/e9d2a983eda2529bb63c93a4e54658ab18ff1af5))
94 | * complete form header ([c5fb147](https://github.com/lbwa/vue-auth-boilerplate/commit/c5fb147b58ac41e411774bf66bb7f874911abf08))
95 | * complete miniprogress bar ([e5275ab](https://github.com/lbwa/vue-auth-boilerplate/commit/e5275ab183b5de149cfe80029dbd45fd857c8b50))
96 | * complete workspace header ([c285dba](https://github.com/lbwa/vue-auth-boilerplate/commit/c285dbaf1ca8b0d35788e089d5a214a590caed3f))
97 | * complete workspace page ([217b4c6](https://github.com/lbwa/vue-auth-boilerplate/commit/217b4c60712e8b32de1f2c8443089e33899d83c1))
98 | * complete workspace projects module ([7da43ce](https://github.com/lbwa/vue-auth-boilerplate/commit/7da43cef6b9bc54660040b37434eafc2b588549f))
99 | * complete workspace radar display & request logic ([1893cb1](https://github.com/lbwa/vue-auth-boilerplate/commit/1893cb111cdec852a38d211cc45a86b56f7d205d))
100 | * create dynamic importer for async components ([f48f91d](https://github.com/lbwa/vue-auth-boilerplate/commit/f48f91de3fc25507015593f3a9258e5330f3848d))
101 | * create public/private routes files ([b42c707](https://github.com/lbwa/vue-auth-boilerplate/commit/b42c7071d1c0ef37967c7ef05632ef6e6b5cf28f))
102 | * dashboard pages dynamic routes ([65d21fe](https://github.com/lbwa/vue-auth-boilerplate/commit/65d21fe4871fe37f14cd0eb5fb316f3cfea71441))
103 | * error404 basic layout ([69e5862](https://github.com/lbwa/vue-auth-boilerplate/commit/69e5862329434060450d66b733785fc70621437f))
104 | * fetch user-info basic logic ([6afe692](https://github.com/lbwa/vue-auth-boilerplate/commit/6afe6929c92122d2892aa104fa5940a564d8881c))
105 | * fetch userInfo logic ([e401afa](https://github.com/lbwa/vue-auth-boilerplate/commit/e401afa5a470f543234f352810c371f8aa9c0c37))
106 | * handle click event of home button in error page ([0e6d3ce](https://github.com/lbwa/vue-auth-boilerplate/commit/0e6d3ce797b132eff62e57d46b9ce31bde8152cd))
107 | * handle keyup.enter event in login form ([471d4f0](https://github.com/lbwa/vue-auth-boilerplate/commit/471d4f097663efbf25ae8752f1db53787e49ae35))
108 | * header-popover base layout ([20324f0](https://github.com/lbwa/vue-auth-boilerplate/commit/20324f04879de615ede3fd4f2e9e79d3dbf6a23b))
109 | * hide child routes functionality in aside ([ccc1698](https://github.com/lbwa/vue-auth-boilerplate/commit/ccc169839b79d814b34f91ef4babb59cc384a223))
110 | * i18n for analysis hot-search ([9e49e08](https://github.com/lbwa/vue-auth-boilerplate/commit/9e49e0850936e07539ea55236e57d55c521b1b8d))
111 | * i18n in analysis page ([677f5ef](https://github.com/lbwa/vue-auth-boilerplate/commit/677f5ef4290f080139f74828d3ebaf79b42d3657))
112 | * import chart-line ([e4afc3c](https://github.com/lbwa/vue-auth-boilerplate/commit/e4afc3c2424bd71779f9fd32ec1e6faccdb2948c))
113 | * info page of step-form display logic ([cedf153](https://github.com/lbwa/vue-auth-boilerplate/commit/cedf153f2cb19398eb56c362748e7a93298ebc56))
114 | * init server ([dae739e](https://github.com/lbwa/vue-auth-boilerplate/commit/dae739e3e75ab4cc0830f9597ed215b46be1c8d6))
115 | * initial building ([c7969d2](https://github.com/lbwa/vue-auth-boilerplate/commit/c7969d2223eda364f99dc90227b489276efc6cca))
116 | * **ability:** build up v-access ([e699e8c](https://github.com/lbwa/vue-auth-boilerplate/commit/e699e8c0c2d6fc56bb22e587a99f15bbea92269f))
117 | * **Ability:** store all routes included private routes ([10be03d](https://github.com/lbwa/vue-auth-boilerplate/commit/10be03d083b9a232898c8955b3ca951fb8f730d7))
118 | * **analysis:** add tooltip ([5097f15](https://github.com/lbwa/vue-auth-boilerplate/commit/5097f1589250a6dd099fd1b894f7da2ad797c760))
119 | * **analysis-middle:** basic layout of analysis__layout ([670d092](https://github.com/lbwa/vue-auth-boilerplate/commit/670d0922c0627c33129d05611af82f117cef7c9c))
120 | * **ananlysis:** tab box shadow and footer font-weight ([1a09ba7](https://github.com/lbwa/vue-auth-boilerplate/commit/1a09ba7ca5fc4dddb2d87f767061d6ef2a72cb1a))
121 | * **api:** switch to mock server ([1ef4dbe](https://github.com/lbwa/vue-auth-boilerplate/commit/1ef4dbe504820d07eb339c0da6a86c44b7ba1b52))
122 | * **app:** ready for new major version ([75cb7fd](https://github.com/lbwa/vue-auth-boilerplate/commit/75cb7fdd625cb8a29b501a18a23e966356ceac10))
123 | * remove `monitor` routes ([70a7335](https://github.com/lbwa/vue-auth-boilerplate/commit/70a73357272c63f010a967e9d7f6b3748f7ae690))
124 | * **app:** use https://github.com/lbwa/adminize-template ([3ebbeb1](https://github.com/lbwa/vue-auth-boilerplate/commit/3ebbeb1f6ce899bdd632633cf740f95909f9f4d9))
125 | * **BaseFooter:** add new time ([de91bfd](https://github.com/lbwa/vue-auth-boilerplate/commit/de91bfd920756333de69472e5ee4f68a4e5b2b3b))
126 | * **constants:** combine all symbolic constants ([f243224](https://github.com/lbwa/vue-auth-boilerplate/commit/f24322467859d0cb23e7a72737251aa946357e5b))
127 | * **flags:** define feature flags ([962696d](https://github.com/lbwa/vue-auth-boilerplate/commit/962696d72fc3381b5063806aa049ca568e1e40b2))
128 | * **global:** migrate from https://github.com/lbwa/adminize-template ([32400fa](https://github.com/lbwa/vue-auth-boilerplate/commit/32400fa992b450e531271a3e47f6776d5b063879))
129 | * **history:** add history bar ([2bd5f50](https://github.com/lbwa/vue-auth-boilerplate/commit/2bd5f506a942fda8c38dc30291ce83d3b3a20152))
130 | * initial project from @vue/cli v4.2.2 ([4cdd0a1](https://github.com/lbwa/vue-auth-boilerplate/commit/4cdd0a18b5a1746b3a31813b6825003dab7ae4e3))
131 | * layout system ([e20f16d](https://github.com/lbwa/vue-auth-boilerplate/commit/e20f16d89732238c3916ddac4c311472caf8fbbb))
132 | * link analysis page to vuex ([c2a3f0d](https://github.com/lbwa/vue-auth-boilerplate/commit/c2a3f0d0b9136e4818c651c94ad496987b993cfb))
133 | * link with store.routes ([c3ba8e1](https://github.com/lbwa/vue-auth-boilerplate/commit/c3ba8e195a566d57e53cd1d1c8c01f9a86424d05))
134 | * link workspace to vuex.store ([923cd0e](https://github.com/lbwa/vue-auth-boilerplate/commit/923cd0e7d66ebe1232573ad1a43c10e9ee2a681b))
135 | * login page basic functionality and style ([9936d32](https://github.com/lbwa/vue-auth-boilerplate/commit/9936d3247e5aee21d225081c3af5bf93880e67f7))
136 | * logout basic logic ([5c134ac](https://github.com/lbwa/vue-auth-boilerplate/commit/5c134ac0ddae4d48b0958de8242770d08ef4fc8a))
137 | * migrate "logout" button ([9335ff3](https://github.com/lbwa/vue-auth-boilerplate/commit/9335ff34aedbcb84c08036419cebf1c3e07c4688))
138 | * mini-progress bar ([6ca66e2](https://github.com/lbwa/vue-auth-boilerplate/commit/6ca66e24a5cf4727b13cd7fafa2603d6c3942311))
139 | * mock data in vuex ([3128483](https://github.com/lbwa/vue-auth-boilerplate/commit/3128483828fbd9fa54f7b8155d4e98392e741747))
140 | * modify root element id ([fad7d30](https://github.com/lbwa/vue-auth-boilerplate/commit/fad7d30fa8f261374fd3da95891445813cd04d03))
141 | * multiple layout ([4b03aec](https://github.com/lbwa/vue-auth-boilerplate/commit/4b03aec79511f6d7d21979e97b8c8a3809acfc47))
142 | * PageHeader components ([488826c](https://github.com/lbwa/vue-auth-boilerplate/commit/488826c5f641ccb0ad09e765958b35dbbb9ae05a))
143 | * reactive doughnut chart ([55eaa82](https://github.com/lbwa/vue-auth-boilerplate/commit/55eaa82b8ade9ef3b9b25ee7248e80cc4186efc4))
144 | * remove redundant route.meta ([d3cfd62](https://github.com/lbwa/vue-auth-boilerplate/commit/d3cfd62953cf0032aa5841e08f876ff8c133bdc1))
145 | * rename user mutation types ([f8a31d2](https://github.com/lbwa/vue-auth-boilerplate/commit/f8a31d26f18257c15ac6e8115efaa26cddf4410c))
146 | * render logic for dynamic routes ([02a4b34](https://github.com/lbwa/vue-auth-boilerplate/commit/02a4b3426a497c6a0411547ef35a7cc40d416682))
147 | * render work with $route.path ([910421b](https://github.com/lbwa/vue-auth-boilerplate/commit/910421b79698dc663dfcf45fe0f69adb38d18c49))
148 | * **sass:** support css module with TS render func ([e0ad877](https://github.com/lbwa/vue-auth-boilerplate/commit/e0ad8770226545b6119a6eade1eb5f99b238cb2e))
149 | * request module for workspace team component ([8ed6ccb](https://github.com/lbwa/vue-auth-boilerplate/commit/8ed6ccbb092118cad28119fec899f7386319343d))
150 | * root__logo component ([224bed0](https://github.com/lbwa/vue-auth-boilerplate/commit/224bed04d7b5f928be5cc60de685977465f7e9b1))
151 | * set token to vuex.store.state ([42c51cb](https://github.com/lbwa/vue-auth-boilerplate/commit/42c51cbe0d784f9ddbd3142d080dab5790a95462))
152 | * step-form submitting logic ([339629d](https://github.com/lbwa/vue-auth-boilerplate/commit/339629d8d7e6772b0603380cce0ea324527465e6))
153 | * step-form transition logic, including nested routes ([8f05c47](https://github.com/lbwa/vue-auth-boilerplate/commit/8f05c47282bc0013489a2760085ced24b582993a))
154 | * step-info page validate logic ([5b79438](https://github.com/lbwa/vue-auth-boilerplate/commit/5b794381f232bafd7e5f998f5b6dcf9012270326))
155 | * step-info store logic ([0a0b6ca](https://github.com/lbwa/vue-auth-boilerplate/commit/0a0b6ca7b9290f3c45698b2ba6203339f62a46b7))
156 | * tab chart in analysis page ([971cd13](https://github.com/lbwa/vue-auth-boilerplate/commit/971cd13e65cc703296c487fdff505446bff47af6))
157 | * toggle user role funcitonality ([29d56d9](https://github.com/lbwa/vue-auth-boilerplate/commit/29d56d9c5166a7ddda3e5c6d6598d2e2a40bf950))
158 | * token validating logic ([e457b99](https://github.com/lbwa/vue-auth-boilerplate/commit/e457b99a5fe8c508371d113f2536e71c9523354c))
159 | * update breadcrumb logic ([5b6d0c1](https://github.com/lbwa/vue-auth-boilerplate/commit/5b6d0c1b332f4ac8274fcb0a96fe48958a5076ff))
160 | * **NavIcon:** modify default nav icon ([32fe41e](https://github.com/lbwa/vue-auth-boilerplate/commit/32fe41ecb45189144d64836bcb8856e650eefc91))
161 | * update routes when toggle user role ([c58454a](https://github.com/lbwa/vue-auth-boilerplate/commit/c58454a80f7665bc217e964d11930f31fec0fee2))
162 | * **history:** avoid append a duplicated history record ([3573663](https://github.com/lbwa/vue-auth-boilerplate/commit/3573663ae754ab526498f391ebe274d4ee797b2e))
163 | * **history:** every routing should append a history ([4e47524](https://github.com/lbwa/vue-auth-boilerplate/commit/4e475241d85d79d208d97e5e4f9843b71fc3255f))
164 | * **history:** impl history linked list ([499450e](https://github.com/lbwa/vue-auth-boilerplate/commit/499450e8ac67db31142bb9f32617561e1693b910))
165 | * **home:** modify home page style ([7b235af](https://github.com/lbwa/vue-auth-boilerplate/commit/7b235afc2a63d3e222757e2f4257d5d49c49df73))
166 | * **Home:** impl Home page ([c82e58d](https://github.com/lbwa/vue-auth-boilerplate/commit/c82e58d2d33500a21478f2aaf9ebed351ac23c1e))
167 | * **layout:** new functional layout component ([5dc458c](https://github.com/lbwa/vue-auth-boilerplate/commit/5dc458ccc96bcd327feee63795c9f6f63301843d))
168 | * **LMainFooter:** modify data fetching ([9eda08f](https://github.com/lbwa/vue-auth-boilerplate/commit/9eda08f4b7c45050b1f920102e7f31009dd3bfe5))
169 | * **LMWF:** support sticky footer ([3b73127](https://github.com/lbwa/vue-auth-boilerplate/commit/3b73127e670c72611827b8191b9cf7638d780bcb))
170 | * **login:** impl how to fetch user abilities ([521a000](https://github.com/lbwa/vue-auth-boilerplate/commit/521a0003326a344cafa0417c8dc6352ed1cc3a77))
171 | * **login:** impl login process ([ad01f80](https://github.com/lbwa/vue-auth-boilerplate/commit/ad01f8064faacc862a498a9bd350e0f2f0f8639d))
172 | * **login:** view style ([045bcd4](https://github.com/lbwa/vue-auth-boilerplate/commit/045bcd4866601490e8cb5c912875b257916d4956))
173 | * **logout:** should reset entire state when logout ([eeb301d](https://github.com/lbwa/vue-auth-boilerplate/commit/eeb301d9d39b14e31a7bd49725e8fe1e1411fdfc))
174 | * **MiddleChartCard:** basic layout ([d165339](https://github.com/lbwa/vue-auth-boilerplate/commit/d16533929f68cf22d86a5978843493e84873e31c))
175 | * **mock:** use devServer to mock apis ([c5cf854](https://github.com/lbwa/vue-auth-boilerplate/commit/c5cf854cb352682b396f2ae2e86fdea4e8dea106))
176 | * **pagination:** basic sort and data filter ([0f68c38](https://github.com/lbwa/vue-auth-boilerplate/commit/0f68c38c488e727554e4d1771b9d542fd6b4beba))
177 | * **Pagination:** basic rendering logic ([4844f91](https://github.com/lbwa/vue-auth-boilerplate/commit/4844f91d7cf28507ab2fc22f78d27ba9dec9c62a))
178 | * **PagingTable:** adjust cell style ([f73c893](https://github.com/lbwa/vue-auth-boilerplate/commit/f73c893515cfc73bbd5c39851392fe1a2746e5e8))
179 | * **PagingTable:** edit/delete cell functionality ([a5ec35d](https://github.com/lbwa/vue-auth-boilerplate/commit/a5ec35db67b968e8d44fc5ebbb653d070d9d2f80))
180 | * **PagingTable:** implement basic functionaliy ([b4e3dda](https://github.com/lbwa/vue-auth-boilerplate/commit/b4e3dda9e5008ab08c986f91ef4f3ee10a9c676b))
181 | * **RAdminize:** impl RAdminize layout component ([230cc6a](https://github.com/lbwa/vue-auth-boilerplate/commit/230cc6af1c74bcdc6bac3a62806ac5ac12ba663e))
182 | * **RAdminize:** RAdminize use dark mode by default ([5addb30](https://github.com/lbwa/vue-auth-boilerplate/commit/5addb30b20495a5dba907b57081d46f6d07eb01a))
183 | * **RAdminize:** recursive component ([cd736bd](https://github.com/lbwa/vue-auth-boilerplate/commit/cd736bd9f52f008e2887ca3dcde0dbde2b571894))
184 | * **router:** guard for detecting login state ([8bfacf7](https://github.com/lbwa/vue-auth-boilerplate/commit/8bfacf738dc2355e4e59576d3164a48387803b05))
185 | * **routes:** add private routes to nav sidebar ([d6660a4](https://github.com/lbwa/vue-auth-boilerplate/commit/d6660a41b60a1061c1c0ffc6e354c0076825c53c))
186 | * **state:** impl presisted state mechanism ([814008f](https://github.com/lbwa/vue-auth-boilerplate/commit/814008f0124f3cd9d7e07d18f221b75b70346054))
187 | * **store:** add strict mode ([30587ba](https://github.com/lbwa/vue-auth-boilerplate/commit/30587ba5a8678dcf38901cbb70f8a6f38e874e74))
188 | * **store:** don't use replaceState anymore ([2b5c7d4](https://github.com/lbwa/vue-auth-boilerplate/commit/2b5c7d4a9c029563cc789318b0b199eda6822b57))
189 | * **store:** import store module automatically ([b1a38bc](https://github.com/lbwa/vue-auth-boilerplate/commit/b1a38bce3caaaed7eae9861e53f4c645ba631855))
190 | * **store:** register store module automatically ([62fcd71](https://github.com/lbwa/vue-auth-boilerplate/commit/62fcd71efb4aa5c067381f02290ea54c54da1f08))
191 | * **style:** support global sass variables ([4caa4c2](https://github.com/lbwa/vue-auth-boilerplate/commit/4caa4c2f12e6817960277b0186751ca3b0667f5f))
192 | * **table:** add routes for table pages ([fae2ba0](https://github.com/lbwa/vue-auth-boilerplate/commit/fae2ba0eda2dc7a8a962bb4c2b54af04b2db542f))
193 | * **type:** migrate RootState type ([933eb34](https://github.com/lbwa/vue-auth-boilerplate/commit/933eb3481b6db65b0356c7a512093af6a48c8ab2))
194 | * **ui:** build up ui framework ([1c336b0](https://github.com/lbwa/vue-auth-boilerplate/commit/1c336b0e4aac16b69f1e545555ef7c70cf5181ce))
195 | * upgrade adminize-template to v0.2.0 ([e2102ca](https://github.com/lbwa/vue-auth-boilerplate/commit/e2102ca61766d042be7f67fa07da28c9786deab8))
196 | * use sessionStorage rather than localStorage ([6326ccd](https://github.com/lbwa/vue-auth-boilerplate/commit/6326ccd6f820914020fbd2b0750fa86cf0fc4376))
197 | * validata token logic in the login page ([7a05173](https://github.com/lbwa/vue-auth-boilerplate/commit/7a0517369d973132531a312c111c8d9153b60419))
198 | * **ui:** use dark mode by default ([7d57d8f](https://github.com/lbwa/vue-auth-boilerplate/commit/7d57d8fec2462f245976f801704e5247260896b7))
199 | * **user:** fill nodes automatically ([66193fb](https://github.com/lbwa/vue-auth-boilerplate/commit/66193fb283a9753d5379bd3330b252a44070e4b4))
200 | * **user:** impl abilities visualization ([518fa4b](https://github.com/lbwa/vue-auth-boilerplate/commit/518fa4b6e9fcb462c9c0f3e60d085c5fddced26e))
201 | * **util:** impl asyncTryCatch ([6b8cfd8](https://github.com/lbwa/vue-auth-boilerplate/commit/6b8cfd8fffee0bb6c573c5d931304c5a04a21bb4))
202 | * workspace navigator card ([fa57aee](https://github.com/lbwa/vue-auth-boilerplate/commit/fa57aeee9962f35713ecd58139ba86059010ccb4))
203 | * workspace radar style ([a69ca42](https://github.com/lbwa/vue-auth-boilerplate/commit/a69ca42b40c4879dbb752b29c780b1458be78b45))
204 |
205 |
206 | ### Bug Fixes
207 |
208 | * app-header box-shadow show up ([5bade8e](https://github.com/lbwa/vue-auth-boilerplate/commit/5bade8e1e4831ba1d9027b876fb365dc8d339946))
209 | * aside log display due to webpack alias ( img src="~a.png") ([078205f](https://github.com/lbwa/vue-auth-boilerplate/commit/078205f4119a6163e53b6ee05d5cbad333bb00f6))
210 | * aside style when hover ([d64c5a1](https://github.com/lbwa/vue-auth-boilerplate/commit/d64c5a1b54ffd123e7e19e7f649a6d910618a35a))
211 | * chart layout in analysis tab ([badc0fd](https://github.com/lbwa/vue-auth-boilerplate/commit/badc0fd63ad9fc007a06210d885d8c3c40a035de))
212 | * correct app-side style ([e352bcd](https://github.com/lbwa/vue-auth-boilerplate/commit/e352bcd6d4839cb7a3a25babec95f86804e8ca01))
213 | * correct collapse style ([8ae3123](https://github.com/lbwa/vue-auth-boilerplate/commit/8ae312359fee69526406516398876c30ee57a4a6))
214 | * correct dashboard render logic by recursive list ([5b7a9f5](https://github.com/lbwa/vue-auth-boilerplate/commit/5b7a9f51de2286d0252ce6c6b082ec256d9a8c71))
215 | * correct deployment config ([103287a](https://github.com/lbwa/vue-auth-boilerplate/commit/103287a43c12594cf755b365c32e8c2c7e054f8e))
216 | * correct directory name for step-form ([3685e26](https://github.com/lbwa/vue-auth-boilerplate/commit/3685e26d6ce1b04b2dcdc246b060bdb99a5f680a))
217 | * **app:** avoid redundant re-rendering with entire app ([9a1b1a4](https://github.com/lbwa/vue-auth-boilerplate/commit/9a1b1a48ecb587b394180dfa93395b62e7de103d)), closes [/github.com/vuejs/vue-router/blob/v3.1.6/src/install.js#L27](https://github.com/lbwa//github.com/vuejs/vue-router/blob/v3.1.6/src/install.js/issues/L27) [/github.com/vuejs/vue/blob/v2.6.11/src/core/global-api/index.js#L41](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/global-api/index.js/issues/L41) [/github.com/vuejs/vue/blob/v2.6.11/src/core/util/index.js#L11](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/util/index.js/issues/L11) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/index.js#L135](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/index.js/issues/L135) [/github.com/vuejs/vue/blob/v2.6.11/src/core/instance/state.js#L187](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/instance/state.js/issues/L187) [/github.com/vuejs/vue/blob/v2.6.11/src/core/instance/state.js#L249](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/instance/state.js/issues/L249) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js#L95](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js/issues/L95) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js#L102](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js/issues/L102) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/dep.js#L58-L61](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/dep.js/issues/L58-L61) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js#L221](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js/issues/L221) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/dep.js#L31-L35](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/dep.js/issues/L31-L35) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js#L128-L137](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js/issues/L128-L137) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/dep.js#L24](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/dep.js/issues/L24) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/index.js#L191](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/index.js/issues/L191) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/dep.js#L37-L49](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/dep.js/issues/L37-L49) [/github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js#L164-L173](https://github.com/lbwa//github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js/issues/L164-L173)
218 | * prevent nested routes cache, (fix [#1](https://github.com/lbwa/vue-auth-boilerplate/issues/1)) ([96f1645](https://github.com/lbwa/vue-auth-boilerplate/commit/96f16458633dbb3db2e5943107d816fa64489deb))
219 | * **history:** handle route without meta field ([609c2f4](https://github.com/lbwa/vue-auth-boilerplate/commit/609c2f49ff2115620d39afc5311ac05a3f9a75d5))
220 | * correct error page layout ([26ae141](https://github.com/lbwa/vue-auth-boilerplate/commit/26ae1413683a6c906c68ea617aad574179a7cdae))
221 | * correct external link in the workspace team component ([f8ecfbe](https://github.com/lbwa/vue-auth-boilerplate/commit/f8ecfbe8efc9cab4537b7c7da0726f822864917a))
222 | * correct re-login nested routes matching error([#1](https://github.com/lbwa/vue-auth-boilerplate/issues/1)) ([1d8bda6](https://github.com/lbwa/vue-auth-boilerplate/commit/1d8bda63df4f693cb52a98af2f760bd90a8c1e12))
223 | * correct redundant initial request(set init tag) ([ef0c4da](https://github.com/lbwa/vue-auth-boilerplate/commit/ef0c4da357514f6ddbc42e6717099f3aa5eea1ca))
224 | * correct redundant request due to nested route initailization ([d1433cd](https://github.com/lbwa/vue-auth-boilerplate/commit/d1433cd4a9b4964a4360b4d6c6d7589ae6fb9c3a))
225 | * correct router filter when user role doesn't include `admin` ([b4949c0](https://github.com/lbwa/vue-auth-boilerplate/commit/b4949c0ff1e9e6a65f4f5a9437d2b77a9c7b6e6c))
226 | * correct routes merge strategy ([c4f759b](https://github.com/lbwa/vue-auth-boilerplate/commit/c4f759bb9b703a1fcae4b7825c87e8f4749c19b0))
227 | * correct routes push when using dynamic components ([10b60ea](https://github.com/lbwa/vue-auth-boilerplate/commit/10b60ea2c25926419100e05fa765068b9772f947))
228 | * correct style of login-footer ([4bebd1a](https://github.com/lbwa/vue-auth-boilerplate/commit/4bebd1a45ea5260be79a61637f392bb9970684a3))
229 | * correct unexpected access ([c439036](https://github.com/lbwa/vue-auth-boilerplate/commit/c4390363f8ae3855c563354a7d738a2a82f65253))
230 | * correct unexpected avatar path in workspace ([6d6db59](https://github.com/lbwa/vue-auth-boilerplate/commit/6d6db59c7d80ab7e71575e504c9e8666b2749025))
231 | * eslint no-unused-vars ([e07584f](https://github.com/lbwa/vue-auth-boilerplate/commit/e07584f278488cc80dd52e26c11715bdd6a13bbe))
232 | * handle network error ([84eb8d6](https://github.com/lbwa/vue-auth-boilerplate/commit/84eb8d66d38e27883cc17067243b7e7cdb64cb01))
233 | * prevent cache nested route temporarily ([31c0d9d](https://github.com/lbwa/vue-auth-boilerplate/commit/31c0d9d71d3c3b7c8bda13edce2d6ca2b7003b30))
234 | * remove old Aside components from Admin layout component ([fe4d099](https://github.com/lbwa/vue-auth-boilerplate/commit/fe4d0992fe6742cd617da8a4659e9845735ade67))
235 | * remove redundant nested routes ([1121557](https://github.com/lbwa/vue-auth-boilerplate/commit/11215577454da9062ec997d9dda4ce34d86c17ca))
236 | * remove redundant validators ([e0d8efa](https://github.com/lbwa/vue-auth-boilerplate/commit/e0d8efa869e323877216c0d7c066f16c8482be3a))
237 | * remove scrollbar-x in the 2nd route transition time ([a4b4928](https://github.com/lbwa/vue-auth-boilerplate/commit/a4b49288cb86bf9b289ca901d58609ad93f873ce))
238 | * reset collapse style ([bd18ab9](https://github.com/lbwa/vue-auth-boilerplate/commit/bd18ab9733e6f9308f69b24f16a405e4944ee781))
239 | * responsive chart (update chart for http response) ([1425140](https://github.com/lbwa/vue-auth-boilerplate/commit/1425140e4841ce108d3079fb95a8b43c04a2a935))
240 | * step/confirm and /success page only work with specific routes ([ccba4e8](https://github.com/lbwa/vue-auth-boilerplate/commit/ccba4e8e7b8e2824735d82e2cae6437f03082b77))
241 | * use hash mode rather than history mode ([7058d34](https://github.com/lbwa/vue-auth-boilerplate/commit/7058d347bf90291929f37ccb7bd4a17a7ddf4304))
242 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Vue auth boilerplate
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | `Vue.js` console boilerplate with authentication.
16 |
17 |
18 |
19 | - [Prerequisites](#prerequisites)
20 | - [Development](#development)
21 | - [Production build](#production-build)
22 | - [CDN supporting](#cdn-supporting)
23 | - [User authentication](#user-authentication)
24 | - [Symbolic constants](#symbolic-constants)
25 | - [Environment variables](#environment-variables)
26 | - [Declarations](#declarations)
27 | - [Layouts](#layouts)
28 | - [Use route-based layout](#use-route-based-layout)
29 | - [Use non-route-based layout](#use-non-route-based-layout)
30 | - [Components](#components)
31 | - [Plugins](#plugins)
32 | - [Effects](#effects)
33 | - [Router](#router)
34 | - [Route config](#route-config)
35 | - [State management](#state-management)
36 | - [Automatic registration](#automatic-registration)
37 | - [With request effects](#with-request-effects)
38 | - [Handle error](#handle-error)
39 | - [History store module](#history-store-module)
40 | - [Changelog](#changelog)
41 | - [License](#license)
42 |
43 |
44 |
45 | ## Prerequisites
46 |
47 | Please make sure you have installed [node.js](https://nodejs.org) version _8.9_ or above (the LTS version recommended).
48 |
49 | ## Development
50 |
51 | - install all dependencies
52 |
53 | ```bash
54 | $ npm i
55 | ```
56 |
57 | - start development server
58 |
59 | ```bash
60 | $ npm run serve
61 | ```
62 |
63 | Frontend server is running at `http://localhost:8080` and `http://:8080` with [hot module replacement](https://webpack.js.org/concepts/hot-module-replacement/).
64 |
65 | - run unit tests
66 |
67 | ```bash
68 | $ npm run test:unit
69 | ```
70 |
71 | ## Production build
72 |
73 | ```bash
74 | $ npm run build
75 | # yarn build
76 | ```
77 |
78 | ### CDN supporting
79 |
80 | This project production build has supported third-party CDN libraries out of the box. All JS/CSS library CDN urls should be recorded by [third-parties.js](./third-parties.js). syntax like:
81 |
82 | ```ts
83 | interface CDNUrl {
84 | name: string // required for js, npm package name
85 | library: string // required for js, global variable name in the browser
86 | js: string // js cdn file urls
87 | css: string // css cdn file urls
88 | }
89 | ```
90 |
91 | - JS libraries
92 |
93 | ```diff
94 | const thirdParties = [
95 | // ... other js/css libraries
96 | + {
97 | + name: 'vue',
98 | + library: 'Vue',
99 | + js: 'https://cdn.jsdelivr.net/npm/vue@2.6.x/dist/vue.min.js'
100 | + }
101 | ]
102 | ```
103 |
104 | - Pure CSS libraries
105 |
106 | step 1. You should import pure css libraries with tree-shaking.
107 |
108 | ```diff
109 | - import 'normalize.css'
110 | + if (__DEV__) { // __DEV__ is an environment variable
111 | + require('normalize.css')
112 | + }
113 | ```
114 |
115 | step 2. add css library CDN link into [third-parties.js](./third-parties.js).
116 |
117 | ```diff
118 | const thirdParties = [
119 | // ... other js/css libraries
120 | + {
121 | + css: 'https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css'
122 | + }
123 | ]
124 | ```
125 |
126 | ## User authentication
127 |
128 | Please refer to `v-access` [documentation](https://github.com/lbwa/v-access#readme).
129 |
130 | ## Symbolic constants
131 |
132 | A symbolic constant is a name given to a constant literal value. It's usually used to prevent [magic numbers][wiki-magic-number] and [hard-coding][wiki-hard-coding]. All symbolic constants should be recorded in the [src/constants.ts](src/constants.ts) file.
133 |
134 | - Q: When should I use symbolic constants?
135 |
136 | - A: [Feature flag](https://en.wikipedia.org/wiki/Feature_toggle), public settings, etc.
137 |
138 | [wiki-magic-number]: https://en.wikipedia.org/wiki/Magic_number_(programming)
139 | [wiki-hard-coding]: https://en.wikipedia.org/wiki/Hard_coding
140 |
141 | ## Environment variables
142 |
143 | All `node.js` use-defined environment should be record at the `/.env*` files and start with `VUE_APP_*` prefix character. You can find more details from the [@vue/cli](https://cli.vuejs.org/guide/mode-and-env.html) documentation.
144 |
145 | ## Declarations
146 |
147 | All global `TypeScript` declarations should be recorded in the [src/global.d.ts](src/global.d.ts) file.
148 |
149 | This boilerplate has supported multiple declarations for CSS pre-process languages in [src/global.d.ts](src/global.d.ts).
150 |
151 | ## Layouts
152 |
153 | In practice, we perhaps have two different kinds of layout components. All layout components should be placed in the [src/layouts](src/layouts) directory.
154 |
155 | | type | description | character |
156 | | :----------------------: | :----------------------: | :------------------: |
157 | | `route-based` layout | with `` | `src/layouts/R*.vue` |
158 | | `non-route-based` layout | without `` | `src/layouts/L*.vue` |
159 |
160 | ### Use route-based layout
161 |
162 | A valid router config should be based on the following structure:
163 |
164 | ```ts
165 | interface ConsoleRouteConfig extends RouteConfig {
166 | meta?: Partial<{
167 | layout: string // which route-based layout need to be rendered
168 | hidden: boolean
169 | icon: string
170 | title: string
171 | }>
172 | }
173 | ```
174 |
175 | We use `meta.layout` to decide which layout component we need to be rendered.
176 |
177 | ### Use non-route-based layout
178 |
179 | As opposed to `route-based` layout, you should always import `non-route-based` layout component manually.
180 |
181 | ```ts
182 | import { NonRouteBasedLayout } from '@/layouts/NonRouteBasedLayout'
183 |
184 | export default {
185 | name: 'AnyViewComponent',
186 |
187 | components: {
188 | NonRouteBasedLayout
189 | }
190 | }
191 | ```
192 |
193 | ## Components
194 |
195 | We also have multiple kinds of components and should be placed in [src/components](src/components) directory.
196 |
197 | | component type | description | example | character |
198 | | :------------: | :-----------------------------------------------: | :-------------------------------: | :------------------: |
199 | | presentational | represent user interface view excluding any state | basic component | `Base*.vue` |
200 | | container | state including all business logic | any sub `views/layouts` component | `[VIEW_PREFIX]*.vue` |
201 |
202 | ## Plugins
203 |
204 | If any `Vue.js` [plugin][doc-vue-plugin] exists, it should be placed in [src/plugins](src/plugins) directory.
205 |
206 | [doc-vue-plugin]: https://vuejs.org/v2/guide/plugins.html
207 |
208 | ## Effects
209 |
210 | All HTTP request function should be placed in [src/effects](src/effects) directory, and should be occurred by [Initiator](src/effects/initiator.ts) instance which has encapsulated `axios` creation and registered two interceptors automatically by default.
211 |
212 | ```ts
213 | import { createInitiator } from './initiator'
214 |
215 | const { http } = createInitiator(/* support all AxiosRequestConfig */)
216 |
217 | export function fetchUserProfile(username: string, password: string) {
218 | return http.post('/user/profile', {
219 | username,
220 | password
221 | })
222 | }
223 | ```
224 |
225 | Based on [single responsibility principle][wiki-single-responsibility-principle] and better universality, every request route should be a singleton. All request or response errors should be handle by Http request consumer, instead of itself.
226 |
227 | [wiki-single-responsibility-principle]: https://en.wikipedia.org/wiki/Single_responsibility_principle
228 |
229 | ## Router
230 |
231 | | filename | description |
232 | | :---------: | :--------------------------------------------------: |
233 | | `guards.ts` | store all [navigation guards][doc-navigation-guards] |
234 | | `index.ts` | export a `vue-router` instance |
235 | | `routes.ts` | record all valid preset static routes |
236 |
237 | [doc-navigation-guards]: https://router.vuejs.org/guide/advanced/navigation-guards.html
238 |
239 | ### Route config
240 |
241 | We add serval `meta` properties for global navigation sidebar.
242 |
243 | ```ts
244 | interface ConsoleRouteConfig extends RouteConfig {
245 | meta?: Partial<{
246 | layout: string // which route-based layout need to be rendered
247 | hidden: boolean
248 | icon: string
249 | title: string
250 | }>
251 | }
252 | ```
253 |
254 | | property name | description |
255 | | :-----------: | :------------------------------------------------------------------------: |
256 | | `meta.layout` | Which [route-based layout](#Use%20route-based%20layout) should be rendered |
257 | | `meta.hidden` | Whether route should be rendered by global navigation sidebar |
258 | | `meta.icon` | Route `material design` icon icon in the global navigation sidebar |
259 | | `meta.title` | Route title in the global navigation sidebar |
260 |
261 | _Note that_ Route only be rendered when `meta.title` and `meta.hidden` is truthy value.
262 |
263 | ## State management
264 |
265 | We use `Vuex` to implement global state management.
266 |
267 | [doc-vuex-actions]: https://vuex.vuejs.org/guide/actions.html#actions
268 |
269 | ### Automatic registration
270 |
271 | All store module placed in [src/store/modules/\*.ts](src/store/modules) would be **registered automatically**. Every module would use their filename as store module namespace.
272 |
273 | ### With request effects
274 |
275 | > [Actions][doc-vuex-actions] can contain arbitrary asynchronous operations.
276 |
277 | All HTTP requests should be called by an action if an HTTP request result needs to be stored in the vuex store, instead of calling HTTP request directly.
278 |
279 | ```
280 | dispatch an action --> http effects --> commit mutation in an action --> store the result of effects
281 | ```
282 |
283 | ### Handle error
284 |
285 | > [store.dispatch][doc-vuex-dispatch] always return a `Promise` instance.
286 |
287 | [doc-vuex-dispatch]: https://vuex.vuejs.org/api/#dispatch
288 |
289 | Any action internal error should be handled by action consumer, instead of action itself.
290 |
291 | ```
292 | action's error --> throw --as rejected promise--> handled by any action consumer
293 | ```
294 |
295 | ### History store module
296 |
297 | We have a `history` ([store/modules/history.ts](src/store/modules/history.ts)) store module that used to record any visited `vue-route record` with `meta.title` field.
298 |
299 | ## Changelog
300 |
301 | All notable changes to this repository will be documented in [CHANGELOG](./CHANGELOG.md) file.
302 |
303 | ## License
304 |
305 | MIT © [Bowen Liu](https://github.com/lbwa)
306 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/cli-plugin-babel/preset']
3 | }
4 |
--------------------------------------------------------------------------------
/mock.config.js:
--------------------------------------------------------------------------------
1 | function createRandomString() {
2 | return Math.random()
3 | .toString(16)
4 | .slice(2)
5 | }
6 |
7 | function Response(code, data) {
8 | this.statusCode = code
9 | this.data = data
10 | }
11 |
12 | const apis = {
13 | /**
14 | * 'METHOD ROUTE': function requestHandler(request, response) {}
15 | */
16 | 'GET /user/abilities': function(req, res) {
17 | res.json(
18 | new Response(
19 | 200,
20 | [
21 | 'github.user.read',
22 | 'github.user.write',
23 | 'github.sample.read',
24 | 'github.sample.write',
25 | 'npm.org.read',
26 | 'npm.org.write',
27 | 'npm.package.read',
28 | 'npm.package.write',
29 | 'github.repo.read',
30 | 'github.repo.write',
31 | 'github.action.read',
32 | 'github.action.write'
33 | ].map(name => ({
34 | name,
35 | uid: createRandomString(),
36 | createAt: Date.now()
37 | }))
38 | )
39 | )
40 | },
41 | 'POST /user/profile': function(req, res) {
42 | res.json(
43 | new Response(200, {
44 | token: createRandomString()
45 | })
46 | )
47 | }
48 | }
49 |
50 | module.exports = function mocker(app) {
51 | const methodsReg = /^(?:GET|POST|DELETE|PUT|PATCH|OPTION)$/i
52 | const routeReg = /^\/.+/
53 | Object.keys(apis).forEach(api => {
54 | const [method, route] = api.split(' ')
55 |
56 | const corsHeaders = {
57 | 'Access-Control-Allow-Origin': '*',
58 | 'Access-Control-Allow-Headers': '*'
59 | }
60 |
61 | app.use(function(request, response, next) {
62 | Reflect.ownKeys(corsHeaders).forEach(header =>
63 | response.setHeader(header, corsHeaders[header])
64 | )
65 | next()
66 | })
67 |
68 | if (methodsReg.test(method) && routeReg.test(route)) {
69 | app[method.toLowerCase()](route, apis[api])
70 | } else {
71 | console.error('[MOCK]: Unexpected method or route')
72 | }
73 | })
74 | }
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-auth-boilerplate",
3 | "version": "3.2.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "report": "set npm_config_report=true && vue-cli-service build",
9 | "test:unit": "vue-cli-service test:unit",
10 | "lint": "vue-cli-service lint"
11 | },
12 | "dependencies": {
13 | "axios": "^0.21.1",
14 | "core-js": "^3.6.4",
15 | "lodash.clonedeep": "^4.5.0",
16 | "normalize.css": "^8.0.1",
17 | "roboto-fontface": "*",
18 | "tiny-invariant": "^1.1.0",
19 | "v-access": "^2.1.1",
20 | "vue": "^2.6.11",
21 | "vue-router": "^3.1.5",
22 | "vuetify": "^2.2.11",
23 | "vuex": "^3.1.2",
24 | "vuex-persistedstate": "^2.7.1"
25 | },
26 | "devDependencies": {
27 | "@types/jest": "^24.0.19",
28 | "@types/lodash.clonedeep": "^4.5.6",
29 | "@typescript-eslint/eslint-plugin": "^2.18.0",
30 | "@typescript-eslint/parser": "^2.18.0",
31 | "@vue/cli-plugin-babel": "~4.2.0",
32 | "@vue/cli-plugin-eslint": "~4.2.0",
33 | "@vue/cli-plugin-router": "~4.2.0",
34 | "@vue/cli-plugin-typescript": "~4.2.0",
35 | "@vue/cli-plugin-unit-jest": "~4.2.0",
36 | "@vue/cli-plugin-vuex": "~4.2.0",
37 | "@vue/cli-service": "~4.2.0",
38 | "@vue/eslint-config-prettier": "^6.0.0",
39 | "@vue/eslint-config-typescript": "^5.0.1",
40 | "@vue/test-utils": "1.0.0-beta.31",
41 | "eslint": "^6.7.2",
42 | "eslint-plugin-prettier": "^3.1.1",
43 | "eslint-plugin-vue": "^6.1.2",
44 | "lint-staged": "^10.1.2",
45 | "material-design-icons-iconfont": "^5.0.1",
46 | "prettier": "^1.19.1",
47 | "sass": "^1.25.0",
48 | "sass-loader": "^8.0.2",
49 | "script-ext-html-webpack-plugin": "^2.1.4",
50 | "typescript": "~3.7.5",
51 | "vue-cli-plugin-vuetify": "~2.0.5",
52 | "vue-template-compiler": "^2.6.11",
53 | "vuetify-loader": "^1.3.0",
54 | "webpack-bundle-analyzer": "^3.7.0"
55 | },
56 | "eslintConfig": {
57 | "root": true,
58 | "env": {
59 | "node": true
60 | },
61 | "extends": [
62 | "plugin:vue/essential",
63 | "eslint:recommended",
64 | "@vue/typescript/recommended",
65 | "@vue/prettier",
66 | "@vue/prettier/@typescript-eslint"
67 | ],
68 | "parserOptions": {
69 | "ecmaVersion": 2020
70 | },
71 | "rules": {},
72 | "globals": {
73 | "__BUILD_TIME__": "readonly",
74 | "__COMMIT_HASH__": "readonly",
75 | "__VERSION__": "readonly"
76 | },
77 | "overrides": [
78 | {
79 | "files": [
80 | "**/__tests__/*.{j,t}s?(x)",
81 | "**/tests/unit/**/*.spec.{j,t}s?(x)"
82 | ],
83 | "env": {
84 | "jest": true
85 | }
86 | }
87 | ]
88 | },
89 | "eslintIgnore": [
90 | "vue.config.js"
91 | ],
92 | "browserslist": [
93 | "> 1%",
94 | "last 2 versions"
95 | ],
96 | "jest": {
97 | "preset": "@vue/cli-plugin-unit-jest/presets/typescript-and-babel"
98 | },
99 | "prettier": {
100 | "semi": false,
101 | "tabWidth": 2,
102 | "printWidth": 80,
103 | "singleQuote": true,
104 | "trailingComma": "none"
105 | },
106 | "gitHooks": {
107 | "pre-commit": "lint-staged"
108 | },
109 | "lint-staged": {
110 | "*.{js,jsx,ts,tsx,vue,json,css,scss,sass,md}": [
111 | "vue-cli-service lint"
112 | ]
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lbwa/vue-auth-boilerplate/11261c51816c06a2c593f7ac367f01b54ceed10f/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 | <% if (htmlWebpackPlugin.options.appVersion) { %>
2 |
3 | <% } %> <% if (htmlWebpackPlugin.options.buildTime) { %>
4 |
5 | <% } %> <% if (htmlWebpackPlugin.options.commitHash) { %>
6 |
7 | <% } %>
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | <% for (var cssUrl of (htmlWebpackPlugin.options.cdnCssUrls || [])) { %>
18 |
19 |
20 | <% } %>
21 |
22 | <% if (htmlWebpackPlugin.options.shouldPreloadCDNFiles) { %> <% for (var
23 | jsUrl of (htmlWebpackPlugin.options.cdnJsUrls || [])) { %>
24 |
25 | <% } %> <% } %>
26 | <%= htmlWebpackPlugin.options.title %>
27 |
28 |
29 |
36 |
37 |
38 |
39 | <% for (var jsUrl of (htmlWebpackPlugin.options.cdnJsUrls || [])) { %>
40 |
41 | <% } %>
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
44 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lbwa/vue-auth-boilerplate/11261c51816c06a2c593f7ac367f01b54ceed10f/src/assets/logo.png
--------------------------------------------------------------------------------
/src/components/BaseFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | Copyright © 2019 - {{ new Date().getFullYear() }}
15 |
16 |
17 |
24 | -
25 |
33 |
34 |
35 |
36 |
41 |
42 |
52 |
--------------------------------------------------------------------------------
/src/components/BaseRouterView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/BaseToast.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 | {{ message }}
9 |
10 | Close
11 |
12 |
13 |
14 |
15 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/RAdminNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/components/RAdminNavDrawer.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 | {{ (route.meta || {}).title }}
17 |
18 |
19 |
20 |
28 |
35 |
36 |
37 |
38 |
39 |
78 |
--------------------------------------------------------------------------------
/src/components/RAdminNavRoot.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ icon }}
5 |
6 |
7 | {{ title }}
8 |
9 |
10 |
11 |
12 |
27 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is used to record all **symbolic constants**.
3 | */
4 |
5 | /**
6 | * the root router-view layout
7 | */
8 | export const DEFAULT_LAYOUT = 'RAdmin'
9 |
10 | /**
11 | * the max value of history records and should be greater than 1
12 | */
13 | export const RECORD_MAX_VAL = 20
14 |
15 | /**
16 | * Used to forbid unauthorized access
17 | */
18 | export const FORBIDDEN_ROUTE = '/forbidden'
19 |
20 | /**
21 | * Used to redirect to target path when login successfully
22 | */
23 | export const QUERY_KEY_FOR_LOGIN_TO = '__from'
24 |
25 | /**
26 | * Default icon for navigation item
27 | */
28 | export const DEFAULT_NAV_ICON = 'work_outline'
29 |
30 | /**
31 | * Base URL for HTTP request
32 | */
33 | export const REQUEST_BASE_URL = process.env.VUE_APP_REQUEST_BASE_URL
34 |
35 | /**
36 | * How long HTTP request should be abort
37 | */
38 | export const REQUEST_TIMEOUT_THRESHOLD = 1000 * 15
39 |
40 | /**
41 | * Key for implementing global persisted state
42 | */
43 | export const PERSISTED_STATE_KEY = '__VX__'
44 |
45 | /**
46 | * Which store namespace should be persisted
47 | */
48 | export const SHOULD_BE_PERSISTED_STATE = ['history', 'user']
49 |
--------------------------------------------------------------------------------
/src/effects/index.ts:
--------------------------------------------------------------------------------
1 | import { createInitiator } from './initiator'
2 | import { REQUEST_BASE_URL, REQUEST_TIMEOUT_THRESHOLD } from '../constants'
3 | import { UserAbility } from '@/store/modules/user'
4 |
5 | const { http } = createInitiator({
6 | baseURL: REQUEST_BASE_URL,
7 | timeout: REQUEST_TIMEOUT_THRESHOLD
8 | })
9 |
10 | const enum Routes {
11 | userLogin = '/user/profile',
12 | userAbilities = '/user/abilities'
13 | }
14 |
15 | export function userLogin(username: string, password: string) {
16 | return http.post, Record<'token', string>>(
17 | Routes.userLogin,
18 | {
19 | username,
20 | password
21 | }
22 | )
23 | }
24 |
25 | export function fetchUserAbilities() {
26 | return http.get(Routes.userAbilities)
27 | }
28 |
--------------------------------------------------------------------------------
/src/effects/initiator.ts:
--------------------------------------------------------------------------------
1 | import axios, {
2 | AxiosRequestConfig,
3 | AxiosInterceptorManager,
4 | AxiosResponse,
5 | AxiosInstance
6 | } from 'axios'
7 | import { isDef } from '@/shared/utils'
8 | import { REQUEST_BASE_URL, REQUEST_TIMEOUT_THRESHOLD } from '../constants'
9 |
10 | type ParamsRequestInterceptors = Parameters<
11 | AxiosInterceptorManager['use']
12 | >
13 |
14 | type ParamsResponseInterceptors = Parameters<
15 | AxiosInterceptorManager>['use']
16 | >
17 |
18 | type OnFulfilledRequest = ParamsRequestInterceptors[0]
19 |
20 | type OnFulfilledResponse = ParamsResponseInterceptors[0]
21 |
22 | type OnRejectedRequestOrResponse = (error: unknown) => unknown
23 |
24 | interface DefaultResponse {
25 | statusCode: 200 | 300 | 400 | 500
26 | data: T
27 | }
28 |
29 | /**
30 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
31 | */
32 | const enum HttpStatus {
33 | OK = 200
34 | }
35 |
36 | /**
37 | * We use dependency injection strategy to implement Axios creation
38 | * NOTE that all request/response error should be handle by consumer for better
39 | * generality (single responsibility principle), not itself.
40 | */
41 | class Initiator {
42 | constructor(private readonly axios: AxiosInstance, autoIntercept = true) {
43 | if (autoIntercept) {
44 | const onerror = (error: Error) => Promise.reject(error)
45 |
46 | this.intercept(
47 | 'request',
48 | config => {
49 | // reject all error response code
50 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
51 | config.validateStatus = status => status <= 400
52 |
53 | return config
54 | },
55 | onerror
56 | )
57 |
58 | /**
59 | * This default response interceptor is based on response structure:
60 | * {
61 | * statusCode: number,
62 | * data: any
63 | * }
64 | */
65 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
66 | this.intercept>(
67 | 'response',
68 | ({ data }) => {
69 | // return raw data when data structure isn't above structure. eg. stream
70 | if (!isDef(data.statusCode)) return data
71 |
72 | // reject all response include statusCode and error message
73 | if (data.statusCode !== HttpStatus.OK) {
74 | return Promise.reject(data)
75 | }
76 |
77 | return data.data
78 | },
79 | onerror
80 | )
81 | }
82 | }
83 |
84 | get http() {
85 | return this.axios
86 | }
87 |
88 | intercept(type: 'request', ...callbacks: ParamsRequestInterceptors): number
89 |
90 | intercept(
91 | type: 'response',
92 | ...callbacks: ParamsResponseInterceptors
93 | ): number
94 |
95 | /**
96 | * https://www.typescriptlang.org/docs/handbook/functions.html#overloads
97 | */
98 | intercept(
99 | type: 'request' | 'response',
100 | onFulfilled?: OnFulfilledRequest & OnFulfilledResponse,
101 | onRejected?: OnRejectedRequestOrResponse
102 | ) {
103 | return this.axios.interceptors[type].use(onFulfilled, onRejected)
104 | }
105 | }
106 |
107 | /**
108 | * Create a http instance
109 | * @param config [Request config](https://github.com/axios/axios#request-config)
110 | * @see [Instance methods](https://github.com/axios/axios#instance-methods)
111 | */
112 | export function createInitiator(
113 | config: AxiosRequestConfig = {
114 | baseURL: REQUEST_BASE_URL,
115 | timeout: REQUEST_TIMEOUT_THRESHOLD
116 | }
117 | ) {
118 | return new Initiator(axios.create(config))
119 | }
120 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * DO NOT import/export any variables from this file for keeping variables globally
3 | *
4 | * In TypeScript, any file containing a top-level import or export is
5 | * considered a module. Conversely, a file without any top-level import or
6 | * export declarations is treated as a script whose contents are available in
7 | * the global scope (and therefore to modules as well).
8 | * @doc https://www.typescriptlang.org/docs/handbook/modules.html
9 | */
10 |
11 | /**
12 | * Whether we are in development environment
13 | */
14 | declare const __DEV__: boolean
15 |
16 | /**
17 | *
18 | * https://github.com/facebook/create-react-app/blob/v3.4.0/packages/react-scripts/lib/react-app.d.ts#L53-L66
19 | */
20 |
21 | declare module '*.module.css' {
22 | const classes: { readonly [key: string]: string }
23 | export default classes
24 | }
25 |
26 | declare module '*.module.scss' {
27 | const classes: { readonly [key: string]: string }
28 | export default classes
29 | }
30 |
31 | declare module '*.module.sass' {
32 | const classes: { readonly [key: string]: string }
33 | export default classes
34 | }
35 |
--------------------------------------------------------------------------------
/src/layouts/RAdmin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vue auth boilerplate console
7 |
8 | Sign out
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{ tab.label }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
80 |
81 |
86 |
--------------------------------------------------------------------------------
/src/layouts/RFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/layouts/RView.module.sass:
--------------------------------------------------------------------------------
1 | .lmf
2 | &__footer
3 | font-size: 12px
4 | text-align: center
5 | padding: 20px 0
6 |
7 | &__link
8 | text-decoration: none
9 |
10 | &:hover
11 | text-decoration: underline
--------------------------------------------------------------------------------
/src/layouts/RView.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { CreateElement, VNode } from 'vue/types/umd'
3 | import classes from './RView.module.sass'
4 |
5 | export default Vue.extend({
6 | name: 'RView',
7 |
8 | functional: true,
9 |
10 | render(h: CreateElement, { parent }): VNode | VNode[] {
11 | const vNodes = [h('router-view')]
12 |
13 | function createAnchorElement(
14 | href: string,
15 | placeholder: string,
16 | isBlank = true
17 | ) {
18 | return h(
19 | 'a',
20 | {
21 | class: {
22 | [classes['lmf__footer__link']]: true
23 | },
24 | attrs: {
25 | href,
26 | target: isBlank ? '_blank' : '_self',
27 | rel: 'noopener noreferrer'
28 | }
29 | },
30 | placeholder
31 | )
32 | }
33 |
34 | const layoutPayload: Record<'sticky', boolean> = (parent.$route.meta || {})
35 | .layoutPayload
36 | if (layoutPayload && layoutPayload.sticky) {
37 | vNodes.push(
38 | h(
39 | 'footer',
40 | {
41 | class: {
42 | [classes['lmf__footer']]: true
43 | }
44 | },
45 | [
46 | createAnchorElement(
47 | 'https://github.com/lbwa/vue-auth-boilerplate',
48 | 'Documentation'
49 | ),
50 | ` - Copyright © ${new Date().getFullYear()} `,
51 | createAnchorElement('https://set.sh', 'Bowen Liu'),
52 | ' - ',
53 | createAnchorElement(
54 | 'https://github.com/lbwa/vue-auth-boilerplate/blob/master/CHANGELOG.md',
55 | 'Changelog'
56 | )
57 | ]
58 | )
59 | )
60 | }
61 |
62 | return vNodes
63 | }
64 | })
65 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import router from './router'
4 | import store from './store'
5 | import { connect } from './plugins/ability'
6 | import App from './App.vue'
7 | import vuetify from './plugins/vuetify'
8 | import './styles/global.sass'
9 | import { FORBIDDEN_ROUTE } from './constants'
10 | import { routes } from './router/private-routes'
11 |
12 | if (__DEV__) {
13 | require('normalize.css')
14 | }
15 |
16 | Vue.config.productionTip = __DEV__
17 |
18 | connect(router, store, { redirect: FORBIDDEN_ROUTE, routes })
19 |
20 | new Vue({
21 | router,
22 | store,
23 | vuetify,
24 | render: h => h(App)
25 | }).$mount('#app')
26 |
--------------------------------------------------------------------------------
/src/plugins/ability.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VAccess, { init } from 'v-access'
3 | import { RootState } from '@/store/global'
4 | import { Store } from 'vuex'
5 | import VueRouter from 'vue-router'
6 | import { userMutationTypes } from '@/store/modules/user'
7 | import { RouteSetting } from '@/router'
8 |
9 | Vue.use(VAccess)
10 |
11 | declare module 'vue-router/types/router' {
12 | interface VueRouter {
13 | options: RouterOptions
14 | }
15 | }
16 |
17 | interface ConnectOptions {
18 | redirect: string
19 | routes: RouteSetting[]
20 | }
21 |
22 | export const connect = function(
23 | vm: Vue | VueRouter,
24 | store: Store,
25 | { redirect, routes }: ConnectOptions = {} as ConnectOptions
26 | ) {
27 | store.watch(
28 | state => state.user.abilities,
29 | abilities => {
30 | if (!Array.isArray(abilities) || !abilities.length) return
31 |
32 | /**
33 | * Because all private routes handle by v-access, we just store routes
34 | * excluding public parts.
35 | */
36 | store.commit(
37 | 'user/' + userMutationTypes.setUserRoutes,
38 | init({
39 | vm,
40 | abilities,
41 | redirect,
42 | routes
43 | })
44 | )
45 | },
46 | {
47 | immediate: true,
48 | sync: true
49 | // type assertion - WatchOption doesn't include `sync` option
50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
51 | } as any
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/src/plugins/persisted-state.ts:
--------------------------------------------------------------------------------
1 | import createPersistedState from 'vuex-persistedstate'
2 | import { PERSISTED_STATE_KEY, SHOULD_BE_PERSISTED_STATE } from '@/constants'
3 |
4 | export default function() {
5 | // You can use any cipher suite with `storage` option
6 | // https://github.com/robinvdvleuten/vuex-persistedstate#encrypted-local-storage
7 | return createPersistedState({
8 | key: PERSISTED_STATE_KEY,
9 | // https://github.com/robinvdvleuten/vuex-persistedstate#createpersistedstateoptions
10 | paths: SHOULD_BE_PERSISTED_STATE
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/src/plugins/store.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from 'vuex'
2 | import { RootState } from '@/store/global'
3 | import cloneDeep from 'lodash.clonedeep'
4 |
5 | /**
6 | * For storing the whole initial `store.state` including all modules states
7 | * @when This plugin will be called only once in the whole Store instance lifetime
8 | * @see https://github.com/vuejs/vuex/blob/v3.1.2/src/store.js#L64-L65
9 | */
10 | export const createResetPlugin: () => Plugin = () => store => {
11 | const clone = cloneDeep(store.state)
12 | delete clone.DO_NOT_MUTATE
13 | store.commit('setSnapshot', clone)
14 | }
15 |
--------------------------------------------------------------------------------
/src/plugins/vuetify.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuetify from 'vuetify/lib'
3 |
4 | if (__DEV__) {
5 | require('material-design-icons-iconfont/dist/material-design-icons.css')
6 | require('roboto-fontface/css/roboto/roboto-fontface.css')
7 | }
8 |
9 | Vue.use(Vuetify)
10 |
11 | export default new Vuetify({
12 | theme: {
13 | dark: true,
14 | options: {
15 | customProperties: true
16 | },
17 | themes: {
18 | light: {
19 | secondary: '#282c34' // '#f34b7d'
20 | }
21 | }
22 | },
23 | icons: {
24 | iconfont: 'md'
25 | }
26 | })
27 |
--------------------------------------------------------------------------------
/src/router/guards.ts:
--------------------------------------------------------------------------------
1 | import { NavigationGuard, Route } from 'vue-router'
2 | import store from '@/store'
3 | import { QUERY_KEY_FOR_LOGIN_TO } from '../constants'
4 |
5 | const WHITE_LIST = ['Login']
6 |
7 | export const onLogin: NavigationGuard = function(to, from, next) {
8 | if (store.getters['user/hasLogin'] || WHITE_LIST.includes(to.name || '')) {
9 | return next()
10 | }
11 | return next({
12 | name: 'Login',
13 | replace: true,
14 | query: {
15 | [QUERY_KEY_FOR_LOGIN_TO]: to.fullPath
16 | }
17 | })
18 | }
19 |
20 | export const onHistoryChange = function(to: Route) {
21 | store.dispatch('history/append', to)
22 | }
23 |
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 | import { RouteWithAbility } from 'v-access'
4 | import { routes } from './public-routes'
5 | import { onLogin, onHistoryChange } from './guards'
6 |
7 | Vue.use(VueRouter)
8 |
9 | export type RouteSetting = /* intersection type */ RouteWithAbility & {
10 | meta?: Partial<{
11 | layout: string
12 | hidden: boolean
13 | icon: string
14 | title: string
15 | }>
16 | }
17 |
18 | const router = new VueRouter({
19 | /**
20 | * Should add new routes in the ./routes.ts file, instead of this file.
21 | */
22 | routes
23 | })
24 |
25 | router.beforeEach(onLogin)
26 | router.afterEach(onHistoryChange)
27 |
28 | export default router
29 |
--------------------------------------------------------------------------------
/src/router/private-routes.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file should record any private routes.
3 | * All of them should be added dynamically
4 | */
5 |
6 | export const routes = [
7 | {
8 | path: '/user',
9 | component: () => import(/* webpackChunkName: 'user' */ '@/views/User.vue'),
10 | meta: {
11 | title: 'User',
12 | ability: 'github.user.read'
13 | }
14 | },
15 | {
16 | path: '/sample',
17 | component: () =>
18 | import(
19 | /* webpackChunkName: 'sample' */ '@/components/BaseRouterView.vue'
20 | ),
21 | meta: {
22 | title: 'Sample'
23 | },
24 | children: [
25 | {
26 | path: 'read',
27 | name: 'SampleRead',
28 | component: () =>
29 | import(/* webpackChunkName: 'sample-read' */ '@/views/Sample.vue'),
30 | meta: {
31 | title: 'SampleRead',
32 | strict: ['github.sample.read', 'npm.org.read', 'npm.org.write']
33 | }
34 | },
35 | {
36 | path: 'read-write',
37 | name: 'SampleReadWrite',
38 | component: () =>
39 | import(/* webpackChunkName: 'sample-write' */ '@/views/Sample.vue'),
40 | meta: {
41 | title: 'SampleWrite',
42 | strict: ['github.sample.read', 'github.sample.write', 'npm.org.read']
43 | }
44 | }
45 | ]
46 | }
47 | ]
48 |
--------------------------------------------------------------------------------
/src/router/public-routes.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file should record any public routes.
3 | * All user would share those static routes
4 | */
5 |
6 | import { RouteSetting } from './index'
7 |
8 | const _routes: RouteSetting[] = [
9 | {
10 | path: '/',
11 | name: 'Home',
12 | component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
13 | },
14 | {
15 | path: '/login',
16 | name: 'Login',
17 | component: () =>
18 | import(/* webpackChunkName: 'login' */ '../views/Login.vue'),
19 | meta: {
20 | layout: 'RFooter'
21 | }
22 | },
23 | {
24 | path: '/forbidden',
25 | name: 'ForbiddenError',
26 | component: () =>
27 | import(/* webpackChunkName: 'error-forbidden' */ '../views/Error.vue'),
28 | props: {
29 | code: 403,
30 | message: 'Forbidden access'
31 | },
32 | meta: {
33 | hidden: true
34 | }
35 | }
36 | ]
37 |
38 | export const routes = _routes.concat({
39 | path: '*',
40 | name: 'NotFoundError',
41 | component: () =>
42 | import(/* webpackChunkName: 'error-not-found' */ '../views/Error.vue'),
43 | props: {
44 | code: 404,
45 | message: 'Seems nothing could be found.'
46 | },
47 | meta: {
48 | layout: 'RView',
49 | hidden: true
50 | }
51 | })
52 |
--------------------------------------------------------------------------------
/src/shared/utils.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-empty-function
2 | function noop() {}
3 |
4 | export const errorLog = __DEV__ ? console.error : noop
5 |
6 | /**
7 | * Whether value is defined
8 | */
9 | export function isDef(val: T): val is NonNullable {
10 | return val !== null && val !== undefined
11 | }
12 |
13 | /**
14 | * handle await statement without `try {} catch (reason) {}`
15 | */
16 | export function asyncTryCatch(promise: Promise) {
17 | return promise
18 | .then<[V, null], [null, R]>(value => [value, null])
19 | .catch<[null, R]>((reason: R) => [null, reason])
20 | }
21 |
--------------------------------------------------------------------------------
/src/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode } from 'vue'
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {}
7 | // tslint:disable no-empty-interface
8 | interface ElementClass extends Vue {}
9 | interface IntrinsicElements {
10 | [elem: string]: any
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue'
3 | export default Vue
4 | }
5 |
--------------------------------------------------------------------------------
/src/store/global.ts:
--------------------------------------------------------------------------------
1 | import { GetterTree, MutationTree, ActionTree } from 'vuex'
2 | import cloneDeep from 'lodash.clonedeep'
3 | import { HistoryState } from './modules/history'
4 | import { UserState } from './modules/user'
5 |
6 | export type RootState = {
7 | DO_NOT_MUTATE: RootState
8 | history: HistoryState
9 | user: UserState
10 | }
11 |
12 | interface GlobalNamespaces {
13 | state: Pick
14 | getters: GetterTree
15 | mutations: MutationTree
16 | actions: ActionTree
17 | }
18 |
19 | const globalNamespace: GlobalNamespaces = {
20 | state: {
21 | // DO NOT mutate this snapshot property for the entire store.state
22 | // This is should be use to reset state
23 | DO_NOT_MUTATE: {} as RootState
24 | },
25 |
26 | getters: {},
27 |
28 | mutations: {
29 | // should always pass a store.state deep clone, instead of reference
30 | setSnapshot(state, snapshot) {
31 | state.DO_NOT_MUTATE = snapshot
32 | },
33 | resetState(state, replacement: RootState) {
34 | /**
35 | * Why we don't use store.replaceState directly?
36 | * https://github.com/lbwa/vue-auth-boilerplate/pull/13
37 | */
38 |
39 | // should always use a deep clone from snapshot to avoid unexpected snapshot mutation
40 | Object.assign(state, cloneDeep(replacement))
41 | }
42 | },
43 |
44 | actions: {
45 | resetState({ commit, rootState }) {
46 | commit('resetState', rootState.DO_NOT_MUTATE)
47 | }
48 | }
49 | }
50 |
51 | export default globalNamespace
52 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import modules from './modules'
5 | import globalNamespace, { RootState } from './global'
6 | import { createResetPlugin } from '../plugins/store'
7 | import createPersistedPlugins from '../plugins/persisted-state'
8 |
9 | Vue.use(Vuex)
10 |
11 | const plugins = (() => {
12 | const plugins = [createResetPlugin(), createPersistedPlugins()]
13 | if (__DEV__) {
14 | // eslint-disable-next-line @typescript-eslint/no-var-requires
15 | plugins.push(require('vuex/dist/logger')())
16 | }
17 | return plugins
18 | })()
19 |
20 | const store = new Vuex.Store(
21 | Object.assign(globalNamespace, {
22 | modules,
23 | strict: __DEV__,
24 | plugins
25 | })
26 | )
27 |
28 | export default store
29 |
--------------------------------------------------------------------------------
/src/store/modules/history.ts:
--------------------------------------------------------------------------------
1 | import { Module } from 'vuex'
2 | import { Route } from 'vue-router'
3 | import invariant from 'tiny-invariant'
4 | import { RootState } from '../global'
5 | import { RECORD_MAX_VAL } from '../../constants'
6 |
7 | export interface HistoryState {
8 | recordHead: RecordItem | null
9 | }
10 |
11 | export class RecordItem {
12 | name?: string
13 | fullPath: string
14 |
15 | constructor(route: Route, public next: RecordItem | null = null) {
16 | this.fullPath = route.fullPath
17 | if (route.meta && route.meta.title) {
18 | this.name = route.meta.title
19 | }
20 | }
21 | }
22 |
23 | const history: Module = {
24 | namespaced: true,
25 |
26 | state: {
27 | recordHead: null
28 | },
29 |
30 | getters: {
31 | isInList({ recordHead }) {
32 | return (route: Route) => {
33 | let current = recordHead
34 | while (current) {
35 | if (current.fullPath === route.fullPath) return true
36 | current = current.next
37 | }
38 |
39 | return false
40 | }
41 | }
42 | },
43 |
44 | mutations: {
45 | append(state, route: Route) {
46 | if (!state.recordHead) {
47 | state.recordHead = new RecordItem(route)
48 | return
49 | }
50 |
51 | let current: RecordItem | null = state.recordHead
52 | let size = 1
53 | invariant(
54 | RECORD_MAX_VAL > 1,
55 | '[module/history]: RECORD_MAX_VAL should be greater then 1'
56 | )
57 | while (current.next) {
58 | ++size
59 | current = current.next
60 | }
61 | // drop the head when exceed
62 | if (size === RECORD_MAX_VAL) {
63 | state.recordHead = state.recordHead.next
64 | }
65 | // a new tail element of linked list
66 | current.next = new RecordItem(route)
67 | },
68 |
69 | prepend(state, route: Route) {
70 | if (!state.recordHead) {
71 | state.recordHead = new RecordItem(route)
72 | return
73 | }
74 |
75 | let current: RecordItem | null = state.recordHead
76 | let size = 1
77 | invariant(
78 | RECORD_MAX_VAL > 1,
79 | '[module/history]: RECORD_MAX_VAL should be greater then 1'
80 | )
81 |
82 | state.recordHead = new RecordItem(route, state.recordHead)
83 |
84 | while (current.next) {
85 | ++size
86 | if (size === RECORD_MAX_VAL) {
87 | // drop the tail element
88 | current.next = null
89 | break
90 | }
91 | current = current.next
92 | }
93 | }
94 | },
95 |
96 | actions: {
97 | append({ commit, getters }, to: Route) {
98 | if (!to.meta || !to.meta.title || getters.isInList(to)) return
99 | // only add it when it doesn't exist in the linked-list
100 | commit('append', to)
101 | }
102 | }
103 | }
104 |
105 | export default history
106 |
--------------------------------------------------------------------------------
/src/store/modules/index.ts:
--------------------------------------------------------------------------------
1 | import { Module } from 'vuex'
2 | import { RootState } from '../global'
3 |
4 | const requireStoreModule = require.context(
5 | '.',
6 | false,
7 | /^\.\/(?!index)\w+\.(t|j)s/ // match all ts file without index.ts
8 | )
9 |
10 | export default requireStoreModule.keys().reduce((map, current) => {
11 | const [, moduleName] = /\.\/([\w-]+)\.ts/.exec(current) || []
12 | if (!map[moduleName]) {
13 | map[moduleName] = requireStoreModule(current).default
14 | }
15 | return map
16 | }, {} as Record>)
17 |
--------------------------------------------------------------------------------
/src/store/modules/user.ts:
--------------------------------------------------------------------------------
1 | import { Module } from 'vuex'
2 | import { RootState } from '../global'
3 | import { Ability, reset } from 'v-access'
4 | import { userLogin, fetchUserAbilities } from '@/effects'
5 | import VueRouter from 'vue-router'
6 | import { RouteSetting } from '@/router'
7 |
8 | export interface UserState {
9 | token: string
10 | abilities: Ability[]
11 | routes: RouteSetting[]
12 | }
13 |
14 | export interface UserAbility {
15 | name: string
16 | uid: string
17 | create_at: string
18 | }
19 |
20 | export const userMutationTypes = {
21 | setToken: 'setToken',
22 | setUserAbilities: 'setUserAbilities',
23 | setUserRoutes: 'setUserRoutes'
24 | }
25 |
26 | const user: Module = {
27 | namespaced: true,
28 |
29 | state: {
30 | token: '',
31 | abilities: [],
32 | // this is user private routes, not all routes
33 | routes: []
34 | },
35 |
36 | getters: {
37 | hasLogin({ token }) {
38 | return Boolean(token)
39 | }
40 | },
41 |
42 | mutations: {
43 | [userMutationTypes.setToken](state, token) {
44 | state.token = token
45 | },
46 | [userMutationTypes.setUserAbilities](state, abilities) {
47 | state.abilities = abilities
48 | },
49 | [userMutationTypes.setUserRoutes](state, routes) {
50 | state.routes = routes
51 | }
52 | },
53 |
54 | actions: {
55 | async login(
56 | { commit },
57 | { username, password }: Record<'username' | 'password', string>
58 | ) {
59 | const { token } = await userLogin(username, password)
60 | token && commit('setToken', token)
61 | },
62 | async fetchUserAbilities({ commit }) {
63 | const abilities = await fetchUserAbilities()
64 |
65 | if (abilities && abilities.length) {
66 | const abilitiesIds = abilities.map(
67 | (ability: Record<'name', string>) => ability.name
68 | )
69 | commit('setUserAbilities', abilitiesIds)
70 | }
71 | },
72 | async logout({ dispatch }, router: VueRouter) {
73 | reset(router)
74 | await dispatch('resetState', null, { root: true })
75 | router.push({
76 | name: 'Login'
77 | })
78 | }
79 | }
80 | }
81 |
82 | export default user
83 |
--------------------------------------------------------------------------------
/src/styles/global.sass:
--------------------------------------------------------------------------------
1 | a.hover-decoration
2 | text-decoration: none
3 |
4 | &:hover
5 | text-decoration: underline
--------------------------------------------------------------------------------
/src/views/Error.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ code }}
5 |
{{ message }}
6 |
7 |
8 |
9 |
10 |
26 |
27 |
48 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Vue auth boilerplate
4 | Vue.js console boilerplate with authentication
5 |
29 |
30 |
31 |
32 |
65 |
66 |
83 |
--------------------------------------------------------------------------------
/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Vue auth boilerplate
14 |
15 |
16 | as user
17 | as admin
18 |
19 |
20 |
21 |
22 |
33 |
48 |
49 |
50 |
51 |
52 |
53 | Login
60 |
61 |
62 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
161 |
162 |
167 |
--------------------------------------------------------------------------------
/src/views/Sample.vue:
--------------------------------------------------------------------------------
1 |
2 | Sample
3 |
4 |
5 |
10 |
11 |
16 |
--------------------------------------------------------------------------------
/src/views/User.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
User center
4 |
5 |
The following trees describe how our routes consist of.
6 |
7 |
20 |
21 |
22 |
23 |
90 |
91 |
105 |
--------------------------------------------------------------------------------
/tests/unit/store/history.spec.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex, { Store } from 'vuex'
3 | import moduleHistory, { RecordItem } from '@/store/modules/history'
4 | import { RECORD_MAX_VAL } from '@/constants'
5 | import { RootState } from '@/store/global'
6 |
7 | Vue.use(Vuex)
8 |
9 | describe('Vuex history module', () => {
10 | let store: Store
11 |
12 | beforeEach(() => {
13 | store = new Vuex.Store({
14 | modules: {
15 | history: Object.assign(moduleHistory, {
16 | // clear state for every test closure
17 | state: {
18 | recordHead: null
19 | }
20 | })
21 | }
22 | })
23 | })
24 |
25 | it("Shouldn't append a history record", () => {
26 | const mockRoute = '/mock?query=append'
27 | store.dispatch('history/append', {
28 | fullPath: mockRoute
29 | })
30 | const { recordHead } = store.state.history
31 | expect(recordHead).toBeNull()
32 | })
33 |
34 | it('Should append a history record', () => {
35 | const mockRoute = '/mock?query=append'
36 | store.dispatch('history/append', {
37 | fullPath: mockRoute,
38 | meta: {
39 | title: 'route-title'
40 | }
41 | })
42 | const { recordHead } = store.state.history
43 | expect((recordHead as RecordItem).fullPath).toEqual(mockRoute)
44 | expect((recordHead as RecordItem).next).toBeNull()
45 | })
46 |
47 | it(`Should avoid append a duplicated history record`, () => {
48 | const route = `/same?query=append`
49 | store.dispatch('history/append', {
50 | fullPath: route,
51 | meta: {
52 | title: 'route-title'
53 | }
54 | })
55 | store.dispatch('history/append', {
56 | fullPath: route,
57 | meta: {
58 | title: 'route-title'
59 | }
60 | })
61 | const { recordHead } = store.state.history
62 | expect((recordHead as RecordItem).next).toBeNull()
63 | })
64 |
65 | it('Should append multiple history records', () => {
66 | const mockFirstRoute = '/mock?query=prepend-one'
67 | const mockSecondRoute = '/mock?query=prepend-two'
68 |
69 | store.dispatch('history/append', {
70 | fullPath: mockFirstRoute,
71 | meta: {
72 | title: 'route-title'
73 | }
74 | })
75 | store.dispatch('history/append', {
76 | fullPath: mockSecondRoute,
77 | meta: {
78 | title: 'route-title'
79 | }
80 | })
81 | const { recordHead } = store.state.history
82 | expect((recordHead as RecordItem).fullPath).toEqual(mockFirstRoute)
83 | expect((recordHead as RecordItem).next).toBeDefined()
84 | expect(((recordHead as RecordItem).next as RecordItem).fullPath).toEqual(
85 | mockSecondRoute
86 | )
87 | expect(((recordHead as RecordItem).next as RecordItem).next).toBeNull()
88 | })
89 |
90 | it('Should append a history record until exceed', () => {
91 | const actualOffset = 5
92 | new Array(RECORD_MAX_VAL + actualOffset)
93 | .fill(null)
94 | .forEach((item, index) => {
95 | store.commit('history/append', {
96 | fullPath: `/mock?query=${index}`
97 | })
98 | })
99 |
100 | const { recordHead } = store.state.history
101 | expect((recordHead as RecordItem).fullPath).toEqual(
102 | `/mock?query=${actualOffset}`
103 | )
104 | expect((recordHead as RecordItem).next).toBeDefined()
105 |
106 | let current: RecordItem = recordHead as RecordItem
107 | let size = 1
108 | while (current.next) {
109 | ++size
110 | current = current.next
111 | }
112 | expect(size).toEqual(RECORD_MAX_VAL)
113 | expect(current.fullPath).toEqual(
114 | `/mock?query=${RECORD_MAX_VAL + actualOffset - 1}`
115 | )
116 | expect(current.next).toBeNull()
117 | })
118 |
119 | it('Should prepend a history record', () => {
120 | const mockRoute = '/mock?query=prepend'
121 | store.commit('history/prepend', {
122 | fullPath: mockRoute
123 | })
124 | const { recordHead } = store.state.history
125 | expect((recordHead as RecordItem).fullPath).toEqual(mockRoute)
126 | expect((recordHead as RecordItem).next).toBeNull()
127 | })
128 |
129 | it('Should prepend multiple history records', () => {
130 | const mockFirstRoute = '/mock?query=prepend-one'
131 | const mockSecondRoute = '/mock?query=prepend-two'
132 |
133 | store.commit('history/prepend', {
134 | fullPath: mockFirstRoute
135 | })
136 | store.commit('history/prepend', {
137 | fullPath: mockSecondRoute
138 | })
139 | const { recordHead } = store.state.history
140 | expect((recordHead as RecordItem).fullPath).toEqual(mockSecondRoute)
141 | expect((recordHead as RecordItem).next).toBeDefined()
142 | expect(((recordHead as RecordItem).next as RecordItem).fullPath).toEqual(
143 | mockFirstRoute
144 | )
145 | expect(((recordHead as RecordItem).next as RecordItem).next).toBeNull()
146 | })
147 |
148 | it('Should prepend a history record until exceed', () => {
149 | const actualOffset = 5
150 | new Array(RECORD_MAX_VAL + actualOffset)
151 | .fill(null)
152 | .forEach((item, index) => {
153 | store.commit('history/prepend', {
154 | fullPath: `/mock?query=${index}`
155 | })
156 | })
157 |
158 | const { recordHead } = store.state.history
159 | expect((recordHead as RecordItem).fullPath).toEqual(
160 | `/mock?query=${RECORD_MAX_VAL + actualOffset - 1}`
161 | )
162 | expect((recordHead as RecordItem).next).toBeDefined()
163 |
164 | let current: RecordItem = recordHead as RecordItem
165 | let size = 1
166 | while (current.next) {
167 | ++size
168 | current = current.next
169 | }
170 | expect(size).toEqual(RECORD_MAX_VAL)
171 | expect(current.fullPath).toEqual(`/mock?query=${actualOffset}`)
172 | expect(current.next).toBeNull()
173 | })
174 | })
175 |
--------------------------------------------------------------------------------
/third-parties.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Record all third-party dependencies that need to be served by CDN.
3 | *
4 | * 1. All these JS dependencies would not be handled by webpack when production
5 | * building.
6 | * 2. ONLY SUPPORT UMD js libraries;
7 | * 3. Pure CSS libraries should work with __DEV__ and require() for
8 | * tree-shaking.
9 | *
10 | * {
11 | * name: '', // required for js, npm package name
12 | * library: '', // required for js, global variable name in the browser
13 | * js: '', // js cdn file urls
14 | * css: '' // css cdn file urls
15 | * }
16 | */
17 |
18 | const thirdParties = [
19 | {
20 | name: 'vue',
21 | library: 'Vue',
22 | js: 'https://cdn.jsdelivr.net/npm/vue@2.6.x/dist/vue.min.js'
23 | },
24 | {
25 | name: 'vue-router',
26 | library: 'VueRouter',
27 | js: 'https://cdn.jsdelivr.net/npm/vue-router@3.1.x/dist/vue-router.min.js'
28 | },
29 | {
30 | name: 'vuex',
31 | library: 'Vuex',
32 | js: 'https://cdn.jsdelivr.net/npm/vuex@3.3.x/dist/vuex.min.js'
33 | },
34 | {
35 | name: 'axios',
36 | library: 'axios',
37 | js: 'https://cdn.jsdelivr.net/npm/axios@0.19.x/dist/axios.min.js'
38 | },
39 | {
40 | name: 'vuex-persistedstate',
41 | library: 'createPersistedState',
42 | js:
43 | 'https://cdn.jsdelivr.net/npm/vuex-persistedstate@3.0.x/dist/vuex-persistedstate.umd.js'
44 | },
45 | // FIXME: duplicated importation
46 | // {
47 | // name: 'vuetify',
48 | // library: 'Vuetify',
49 | // js: 'https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.js',
50 | // css: 'https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css'
51 | // },
52 | {
53 | css:
54 | 'https://cdn.jsdelivr.net/npm/material-design-icons-iconfont@5.0.x/dist/material-design-icons.min.css'
55 | },
56 | {
57 | css:
58 | 'https://cdn.jsdelivr.net/npm/roboto-fontface@0.10.0/css/roboto/roboto-fontface.min.css'
59 | },
60 | {
61 | css: 'https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css'
62 | }
63 | ]
64 |
65 | // DO NOT MODIFY the following codes if you don't know what's going on.
66 |
67 | // use to be webpackConfig.externals
68 | // https://webpack.js.org/configuration/externals/
69 | const externals = {}
70 | // inject these urls to html template
71 | const urls = thirdParties.reduce(
72 | (urls, library) => {
73 | if (library.js) {
74 | urls.js.push(library.js)
75 | }
76 | if (library.css) {
77 | urls.css.push(library.css)
78 | }
79 | // set webpackConfig.externals
80 | if (library.js && library.name && library.library) {
81 | externals[library.name] = library.library
82 | }
83 | return urls
84 | },
85 | {
86 | js: [],
87 | css: []
88 | }
89 | )
90 |
91 | exports.externals = externals
92 | exports.thirdPartiesUrls = urls
93 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "esModuleInterop": true,
10 | "allowSyntheticDefaultImports": true,
11 | "sourceMap": true,
12 | "baseUrl": ".",
13 | "types": [
14 | "webpack-env",
15 | "jest",
16 | "vue-router",
17 | "vuex",
18 | "v-access",
19 | "vuetify"
20 | ],
21 | "paths": {
22 | "@/*": [
23 | "src/*"
24 | ]
25 | },
26 | "lib": [
27 | "esnext",
28 | "dom",
29 | "dom.iterable",
30 | "scripthost"
31 | ]
32 | },
33 | "include": [
34 | "src/**/*.ts",
35 | "src/**/*.tsx",
36 | "src/**/*.vue",
37 | "tests/**/*.ts",
38 | "tests/**/*.tsx"
39 | ],
40 | "exclude": [
41 | "node_modules"
42 | ]
43 | }
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * SHOULD read https://cli.vuejs.org first to modify these configurations.
3 | */
4 | const { externals, thirdPartiesUrls } = require('./third-parties.js')
5 | const mocker = require('./mock.config.js')
6 | const appVersion = require('./package.json').version
7 |
8 | const __DEV__ = process.env.NODE_ENV === 'development'
9 | const __PROD__ = process.env.NODE_ENV === 'production'
10 | const commitHash = { value: '' }
11 | const buildTime = new Date().toLocaleString()
12 |
13 | try {
14 | commitHash.value = require('child_process')
15 | .execSync('git rev-parse HEAD')
16 | .toString()
17 | } catch (err) {
18 | // current workspace isn't git repository
19 | commitHash.value = ''
20 | }
21 |
22 | // should we enable preload feature for CDN files
23 | const shouldPreloadCDNFiles = true
24 |
25 | module.exports = {
26 | publicPath: './',
27 |
28 | productionSourceMap: false,
29 |
30 | chainWebpack(config) {
31 | /**
32 | * Feature flags in **compiler** time
33 | * @doc https://webpack.js.org/plugins/define-plugin/#feature-flags
34 | */
35 | config.plugin('define').tap(([args]) => {
36 | args.__DEV__ = JSON.stringify(__DEV__)
37 | args.__BUILD_TIME__ = JSON.stringify(buildTime)
38 | args.__VERSION__ = JSON.stringify(appVersion)
39 | args.__COMMIT_HASH__ = JSON.stringify(commitHash.value)
40 | return [args]
41 | })
42 |
43 | /**
44 | * resolve eslint-loader with symlink package error temporarily
45 | * "No eslint configuration file ... at symlink package"
46 | * https://webpack.js.org/configuration/resolve/#resolvesymlinks
47 | */
48 | config.resolve.symlinks(false)
49 |
50 | // Only works with production building
51 | config.when(__PROD__, config => {
52 | // use third-parties with cdn files. instead webpack chunk files
53 | config.plugin('html').tap(([args]) => {
54 | args.cdnCssUrls = thirdPartiesUrls.css || []
55 | args.cdnJsUrls = thirdPartiesUrls.js || []
56 | args.shouldPreloadCDNFiles = shouldPreloadCDNFiles
57 | args.appVersion = appVersion
58 | args.buildTime = buildTime
59 | args.commitHash = commitHash.value
60 | return [args]
61 | })
62 | config.externals(externals)
63 |
64 | /**
65 | * extract and inline webpack runtime code
66 | * @see https://webpack.js.org/configuration/optimization/#optimizationruntimechunk
67 | * @further https://github.com/facebook/create-react-app/blob/v3.4.1/packages/react-dev-utils/InlineChunkHtmlPlugin.js
68 | */
69 | config.optimization.runtimeChunk('single')
70 | config.plugin('preload').tap(([args]) => {
71 | args.fileBlacklist = (args.fileBlacklist || []).concat(
72 | /runtime\..+\.js$/i
73 | )
74 | return [args]
75 | })
76 | config
77 | .plugin('ScriptExtHtmlWebpackPlugin')
78 | .before('preload')
79 | .use(require('script-ext-html-webpack-plugin'), [
80 | { inline: /runtime\..+\.js$/i }
81 | ])
82 |
83 | // config terser plugin options
84 | config.optimization.minimizer('terser').tap(([options]) => {
85 | options.terserOptions.compress.drop_console = true
86 | return [options]
87 | })
88 | })
89 |
90 | // webpack bundles analyzer
91 | config.when(process.env.npm_config_report, config => {
92 | config
93 | .plugin('webpack-bundle-analyzer')
94 | .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
95 | })
96 | },
97 |
98 | devServer: {
99 | before(app) {
100 | mocker(app)
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------