├── .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 | github actions - tests 6 | 7 | 8 | github actions - deployment 9 | 10 | 11 | nodejs version 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 | 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 | 35 | 36 | 41 | 42 | 52 | -------------------------------------------------------------------------------- /src/components/BaseRouterView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/BaseToast.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/RAdminNav.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/components/RAdminNavDrawer.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 78 | -------------------------------------------------------------------------------- /src/components/RAdminNavRoot.vue: -------------------------------------------------------------------------------- 1 | 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 | 42 | 43 | 80 | 81 | 86 | -------------------------------------------------------------------------------- /src/layouts/RFooter.vue: -------------------------------------------------------------------------------- 1 | 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 | 9 | 10 | 26 | 27 | 48 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 65 | 66 | 83 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 161 | 162 | 167 | -------------------------------------------------------------------------------- /src/views/Sample.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/views/User.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------