From 04fc5f2b8d1be3688290c2692f93fbcb211150fa Mon Sep 17 00:00:00 2001 From: Leo Q Date: Mon, 22 Nov 2021 15:13:02 +0800 Subject: [PATCH] Nuxt frontend (#100) * rework frontend with nuxt * replace npm proxy to official npm * fix ci * add more implements * clean unused config and views * add cache stage for release workflow * edit helpdesk title --- .github/workflows/release.yml | 14 +- .github/workflows/test.yml | 27 +- README.md | 2 +- frontend/.dockerignore | 2 +- frontend/.editorconfig | 13 + frontend/.eslintrc.js | 20 + frontend/.gitignore | 106 +- frontend/.prettierrc | 4 + frontend/README.md | 81 +- frontend/babel.config.js | 5 - frontend/components/DynamicForm.vue | 36 + .../components/FormWidgets/CheckBoxInput.vue | 14 +- .../components/FormWidgets/NumberInput.vue | 2 +- .../components/FormWidgets/SelectInput.vue | 14 +- .../components/FormWidgets/TextInput.vue | 8 +- frontend/{src => }/components/HActionView.vue | 11 - frontend/{src => }/components/HDrawer.vue | 29 +- frontend/{src => }/components/HFooter.vue | 0 frontend/{src => }/components/HForm.vue | 77 +- frontend/{src => }/components/HHeader.vue | 20 +- frontend/{src => }/components/HSider.vue | 41 +- .../{src => }/components/HTicketResult.vue | 57 +- frontend/{src => }/components/Hdag.vue | 40 +- frontend/components/NotifyCard.vue | 20 + .../{src => }/components/ResultHostTable.vue | 62 +- frontend/{src => }/components/SubMenu.vue | 2 +- frontend/{src => }/components/SubTab.vue | 10 +- .../components/SubTabNoRecursive.vue | 12 +- frontend/jest.config.js | 3 - frontend/layouts/blank.vue | 7 + frontend/layouts/default.vue | 18 + frontend/nuxt.config.js | 72 + frontend/package-lock.json | 39291 +++++++--------- frontend/package.json | 86 +- frontend/pages/_action/index.vue | 24 + .../components/HHome.vue => pages/index.vue} | 22 +- .../components/Login.vue => pages/login.vue} | 30 +- .../ticket/_id.vue} | 78 +- .../ticket/index.vue} | 77 +- frontend/plugins/antd.js | 4 + frontend/plugins/axios.js | 31 + frontend/plugins/notify.js | 6 + frontend/plugins/text-highlight.js | 4 + frontend/plugins/vue-select.js | 5 + frontend/public/index.html | 17 - frontend/src/App.vue | 12 - frontend/src/assets/logo.png | Bin 6849 -> 0 bytes frontend/src/assets/vue-select.min.css | 3 - frontend/src/components/DynamicForm.vue | 39 - frontend/src/components/HBase.vue | 26 - frontend/src/main.js | 31 - frontend/src/plugins/ant-design-vue.js | 4 - frontend/src/router/index.js | 42 - frontend/src/store/index.js | 62 - frontend/src/utils/HRequests.js | 48 - frontend/{public => static}/favicon.ico | Bin frontend/store/README.md | 10 + frontend/store/alert.js | 13 + frontend/store/index.js | 56 + frontend/tests/unit/HFinder.spec.js | 72 - frontend/{src => }/utils/HComparer.js | 0 frontend/{src => }/utils/HDate.js | 2 +- frontend/{src => }/utils/HFinder.js | 14 +- frontend/vue.config.js | 14 - helpdesk/__init__.py | 2 + helpdesk/config.py | 11 - helpdesk/libs/dependency.py | 99 +- helpdesk/views/auth/index.py | 43 +- requirements.txt | 2 +- 69 files changed, 18355 insertions(+), 22754 deletions(-) create mode 100644 frontend/.editorconfig create mode 100644 frontend/.eslintrc.js create mode 100644 frontend/.prettierrc delete mode 100644 frontend/babel.config.js create mode 100644 frontend/components/DynamicForm.vue rename frontend/{src => }/components/FormWidgets/CheckBoxInput.vue (100%) rename frontend/{src => }/components/FormWidgets/NumberInput.vue (100%) rename frontend/{src => }/components/FormWidgets/SelectInput.vue (81%) rename frontend/{src => }/components/FormWidgets/TextInput.vue (86%) rename frontend/{src => }/components/HActionView.vue (67%) rename frontend/{src => }/components/HDrawer.vue (86%) rename frontend/{src => }/components/HFooter.vue (100%) rename frontend/{src => }/components/HForm.vue (83%) rename frontend/{src => }/components/HHeader.vue (74%) rename frontend/{src => }/components/HSider.vue (73%) rename frontend/{src => }/components/HTicketResult.vue (77%) rename frontend/{src => }/components/Hdag.vue (74%) create mode 100644 frontend/components/NotifyCard.vue rename frontend/{src => }/components/ResultHostTable.vue (88%) rename frontend/{src => }/components/SubMenu.vue (92%) rename frontend/{src => }/components/SubTab.vue (66%) rename frontend/{src => }/components/SubTabNoRecursive.vue (64%) delete mode 100644 frontend/jest.config.js create mode 100644 frontend/layouts/blank.vue create mode 100644 frontend/layouts/default.vue create mode 100644 frontend/nuxt.config.js create mode 100644 frontend/pages/_action/index.vue rename frontend/{src/components/HHome.vue => pages/index.vue} (55%) rename frontend/{src/components/Login.vue => pages/login.vue} (72%) rename frontend/{src/components/HTicketDetail.vue => pages/ticket/_id.vue} (85%) rename frontend/{src/components/HTicketList.vue => pages/ticket/index.vue} (85%) create mode 100644 frontend/plugins/antd.js create mode 100644 frontend/plugins/axios.js create mode 100644 frontend/plugins/notify.js create mode 100644 frontend/plugins/text-highlight.js create mode 100644 frontend/plugins/vue-select.js delete mode 100644 frontend/public/index.html delete mode 100644 frontend/src/App.vue delete mode 100644 frontend/src/assets/logo.png delete mode 100644 frontend/src/assets/vue-select.min.css delete mode 100644 frontend/src/components/DynamicForm.vue delete mode 100644 frontend/src/components/HBase.vue delete mode 100644 frontend/src/main.js delete mode 100644 frontend/src/plugins/ant-design-vue.js delete mode 100644 frontend/src/router/index.js delete mode 100644 frontend/src/store/index.js delete mode 100644 frontend/src/utils/HRequests.js rename frontend/{public => static}/favicon.ico (100%) create mode 100644 frontend/store/README.md create mode 100644 frontend/store/alert.js create mode 100644 frontend/store/index.js delete mode 100644 frontend/tests/unit/HFinder.spec.js rename frontend/{src => }/utils/HComparer.js (100%) rename frontend/{src => }/utils/HDate.js (69%) rename frontend/{src => }/utils/HFinder.js (82%) delete mode 100644 frontend/vue.config.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e42b0345..e15df4d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,14 +21,18 @@ jobs: - name: Check out code uses: actions/checkout@v2 - - - run: npm install + - name: Cache node_modules 📦 + uses: actions/cache@v2.1.4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - run: npm ci --prefer-offline --no-audit working-directory: frontend - run: npm run lint working-directory: frontend - - run: npm run test:unit - working-directory: frontend - - run: npm run build + - run: npm run generate working-directory: frontend - name: Upload a Build Artifact uses: actions/upload-artifact@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c5a0931..c310f9ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,18 +10,29 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v1 + - name: Checkout 🛎 + uses: actions/checkout@master + + - name: Setup node env 🏗 + uses: actions/setup-node@v2.1.5 with: - node-version: '12' + node-version: ${{ matrix.node }} + check-latest: true - - name: Check out code - uses: actions/checkout@v2 + - name: Cache node_modules 📦 + uses: actions/cache@v2.1.4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- - - run: npm install - working-directory: frontend - - run: npm run lint + - name: Install dependencies 👨🏻‍💻 + run: npm ci --prefer-offline --no-audit working-directory: frontend - - run: npm run test:unit + + - name: Run linter 👀 + run: npm run lint working-directory: frontend backend: name: Build backend diff --git a/README.md b/README.md index 2231fe1c..506ceec7 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ First make sure you have installed latest [nodejs](https://nodejs.org/en/downloa ``` cd frontend npm install -npm start +npm run dev ``` Follow the link in the console. diff --git a/frontend/.dockerignore b/frontend/.dockerignore index b512c09d..3c3629e6 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -1 +1 @@ -node_modules \ No newline at end of file +node_modules diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 00000000..5d126348 --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js new file mode 100644 index 00000000..fd4ca4f8 --- /dev/null +++ b/frontend/.eslintrc.js @@ -0,0 +1,20 @@ +module.exports = { + root: true, + env: { + browser: true, + node: true + }, + parserOptions: { + parser: '@babel/eslint-parser', + requireConfigFile: false + }, + extends: [ + '@nuxtjs', + 'plugin:nuxt/recommended', + 'prettier' + ], + plugins: [ + ], + // add your custom rules here + rules: {} +} diff --git a/frontend/.gitignore b/frontend/.gitignore index 0ae2ddf7..e8f682ba 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,26 +1,90 @@ -.DS_Store -node_modules -/dist - -# local env files -.env.local -.env.*.local - -# Log files +# Created by .ignore support plugin (hsz.mobi) +### Node template +# Logs +/logs +*.log npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* -# Editor directories and files +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# Nuxt generate +dist + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# IDE / Editor .idea -.vscode -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# test reports -/test/unit/coverage/ -/test/e2e/reports/ + +# Service worker +sw.* + +# macOS +.DS_Store + +# Vim swap files +*.swp diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/frontend/README.md b/frontend/README.md index c932a9c9..43bdbdac 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,24 +1,69 @@ -# helpdesk +# helpdesk-frontend -## Project setup -``` -npm install -``` +## Build Setup -### Compiles and hot-reloads for development -``` -npm run serve -``` +```bash +# install dependencies +$ npm install -### Compiles and minifies for production -``` -npm run build -``` +# serve with hot reload at localhost:3000 +$ npm run dev -### Lints and fixes files -``` -npm run lint +# build for production and launch server +$ npm run build +$ npm run start + +# generate static project +$ npm run generate ``` -### Customize configuration -See [Configuration Reference](https://cli.vuejs.org/config/). +For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org). + +## Special Directories + +You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality. + +### `assets` + +The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets). + +### `components` + +The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components). + +### `layouts` + +Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts). + + +### `pages` + +This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing). + +### `plugins` + +The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins). + +### `static` + +This directory contains your static files. Each file inside this directory is mapped to `/`. + +Example: `/static/robots.txt` is mapped as `/robots.txt`. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static). + +### `store` + +This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store). diff --git a/frontend/babel.config.js b/frontend/babel.config.js deleted file mode 100644 index e9558405..00000000 --- a/frontend/babel.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - presets: [ - '@vue/cli-plugin-babel/preset' - ] -} diff --git a/frontend/components/DynamicForm.vue b/frontend/components/DynamicForm.vue new file mode 100644 index 00000000..d8b840c5 --- /dev/null +++ b/frontend/components/DynamicForm.vue @@ -0,0 +1,36 @@ + + + diff --git a/frontend/src/components/FormWidgets/CheckBoxInput.vue b/frontend/components/FormWidgets/CheckBoxInput.vue similarity index 100% rename from frontend/src/components/FormWidgets/CheckBoxInput.vue rename to frontend/components/FormWidgets/CheckBoxInput.vue index 3295d2a1..fe460cec 100644 --- a/frontend/src/components/FormWidgets/CheckBoxInput.vue +++ b/frontend/components/FormWidgets/CheckBoxInput.vue @@ -6,9 +6,9 @@ :wrapper-col="{ span: 12 }" > @@ -18,11 +18,6 @@ export default { name: 'CheckBoxInput', props: ['label', 'name', 'value', 'extra'], - methods: { - onCheckBoxChange (event) { - this.$emit('input', event.target.checked) - } - }, computed: { decorator () { return [ @@ -30,6 +25,11 @@ export default { rules: [{required: this.required, message: 'This field is required'}] }] } + }, + methods: { + onCheckBoxChange (event) { + this.$emit('input', event.target.checked) + } } } diff --git a/frontend/src/components/FormWidgets/NumberInput.vue b/frontend/components/FormWidgets/NumberInput.vue similarity index 100% rename from frontend/src/components/FormWidgets/NumberInput.vue rename to frontend/components/FormWidgets/NumberInput.vue index f6a2adc1..ac8460c9 100644 --- a/frontend/src/components/FormWidgets/NumberInput.vue +++ b/frontend/components/FormWidgets/NumberInput.vue @@ -5,10 +5,10 @@ :wrapper-col="{ span: 12 }" > diff --git a/frontend/src/components/FormWidgets/SelectInput.vue b/frontend/components/FormWidgets/SelectInput.vue similarity index 81% rename from frontend/src/components/FormWidgets/SelectInput.vue rename to frontend/components/FormWidgets/SelectInput.vue index d0e547bb..00d8bd32 100644 --- a/frontend/src/components/FormWidgets/SelectInput.vue +++ b/frontend/components/FormWidgets/SelectInput.vue @@ -13,37 +13,33 @@ :searchable="true" :filterable="true" :select-on-tab="true" + :value="selectedValues" @input="handleInput" @keypress.enter.native.prevent="" - :value="selectedValues" > - + diff --git a/frontend/src/components/Hdag.vue b/frontend/components/Hdag.vue similarity index 74% rename from frontend/src/components/Hdag.vue rename to frontend/components/Hdag.vue index 8cdb2159..071a02a3 100644 --- a/frontend/src/components/Hdag.vue +++ b/frontend/components/Hdag.vue @@ -8,11 +8,19 @@ import go from 'gojs' export default { name: "Hdag", - props: ["modelData", "selectedNode"], // accept model data as a parameter - mounted: function() { - var $ = go.GraphObject.make; - var self = this; - var myDiagram = + props: ["modelData", "selectedNode"], + watch: { + modelData(val) { this.updateModel(val); }, + selectedNode(newV) { + const node = this.diagram.findNodeForKey(newV) + this.diagram.select(node) + + }, + }, // accept model data as a parameter + mounted() { + const $ = go.GraphObject.make; + const self = this; + const myDiagram = $(go.Diagram, this.$el, { initialContentAlignment: go.Spot.Center, @@ -20,8 +28,8 @@ export default { "undoManager.isEnabled": false, isReadOnly: true, // Model ChangedEvents get passed up to component users - "ModelChanged": function(e) { self.$emit("model-changed", e); }, - "ChangedSelection": function(e) { self.$emit("changed-selection", e); } + "ModelChanged"(e) { self.$emit("model-changed", e); }, + "ChangedSelection"(e) { self.$emit("changed-selection", e); } }); myDiagram.nodeTemplate = @@ -48,31 +56,23 @@ export default { this.diagram = myDiagram; this.updateModel(this.modelData); }, - watch: { - modelData: function(val) { this.updateModel(val); }, - selectedNode: function(newV) { - var node = this.diagram.findNodeForKey(newV) - this.diagram.select(node) - - }, - }, methods: { - model: function() { return this.diagram.model; }, - updateModel: function(val) { + model() { return this.diagram.model; }, + updateModel(val) { // No GoJS transaction permitted when replacing Diagram.model. if (val instanceof go.Model) { this.diagram.model = val; } else { - var m = new go.GraphLinksModel() + const m = new go.GraphLinksModel() if (val) { - for (var p in val) { + for (const p in val) { m[p] = val[p]; } } this.diagram.model = m; } }, - updateDiagramFromData: function() { + updateDiagramFromData() { this.diagram.startTransaction(); // This is very general but very inefficient. // It would be better to modify the diagramData data by calling diff --git a/frontend/components/NotifyCard.vue b/frontend/components/NotifyCard.vue new file mode 100644 index 00000000..fb867ebb --- /dev/null +++ b/frontend/components/NotifyCard.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/frontend/src/components/ResultHostTable.vue b/frontend/components/ResultHostTable.vue similarity index 88% rename from frontend/src/components/ResultHostTable.vue rename to frontend/components/ResultHostTable.vue index f48b1fe1..e5874dc0 100644 --- a/frontend/src/components/ResultHostTable.vue +++ b/frontend/components/ResultHostTable.vue @@ -4,31 +4,31 @@

{{totalCount}} execution(s) in total , {{successCount}} succeed, {{failedCount}} failed .

+ @expand="handleRowExpand">
Search Reset
@@ -41,7 +41,7 @@

stderr:

-              
+              
                 {{typeof(record.stderr) === 'string' ? record.stderr : ''}}
               
             
@@ -61,12 +61,9 @@ diff --git a/frontend/src/components/SubMenu.vue b/frontend/components/SubMenu.vue similarity index 92% rename from frontend/src/components/SubMenu.vue rename to frontend/components/SubMenu.vue index a8b948cc..0e287f81 100644 --- a/frontend/src/components/SubMenu.vue +++ b/frontend/components/SubMenu.vue @@ -12,7 +12,7 @@ :key="item.key" > {{item.name}} diff --git a/frontend/src/components/SubTab.vue b/frontend/components/SubTab.vue similarity index 66% rename from frontend/src/components/SubTab.vue rename to frontend/components/SubTab.vue index 2cb1769f..1165f316 100644 --- a/frontend/src/components/SubTab.vue +++ b/frontend/components/SubTab.vue @@ -1,16 +1,16 @@ diff --git a/frontend/src/components/SubTabNoRecursive.vue b/frontend/components/SubTabNoRecursive.vue similarity index 64% rename from frontend/src/components/SubTabNoRecursive.vue rename to frontend/components/SubTabNoRecursive.vue index f5f1858d..d1e9ef72 100644 --- a/frontend/src/components/SubTabNoRecursive.vue +++ b/frontend/components/SubTabNoRecursive.vue @@ -1,17 +1,17 @@ - - diff --git a/frontend/src/components/HBase.vue b/frontend/src/components/HBase.vue deleted file mode 100644 index f81e2854..00000000 --- a/frontend/src/components/HBase.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/frontend/src/main.js b/frontend/src/main.js deleted file mode 100644 index ae5d4e01..00000000 --- a/frontend/src/main.js +++ /dev/null @@ -1,31 +0,0 @@ -// The Vue build version to load with the `import` command -// (runtime-only or standalone) has been set in webpack.base.conf with an alias. -import Vue from 'vue' -import App from './App' -import router from './router' -import store from './store' -import './assets/vue-select.min.css' -import './plugins/ant-design-vue.js' -import {tokenInterceptor, HRequest} from "./utils/HRequests"; -import VueKeyCloak from '@dsb-norge/vue-keycloak-js' - -/* eslint-disable no-new */ -let vm = new Vue({ - router, - store, - render: h => h(App) -}) - -Vue.config.productionTip = false -Vue.use(VueKeyCloak, { - init: { - onLoad: 'login-required' - }, - config: "/auth/oidc-configs.json", - onReady: () => { - tokenInterceptor(HRequest) - vm.$mount('#app') - } -}) - -export {vm} diff --git a/frontend/src/plugins/ant-design-vue.js b/frontend/src/plugins/ant-design-vue.js deleted file mode 100644 index b6aa9aac..00000000 --- a/frontend/src/plugins/ant-design-vue.js +++ /dev/null @@ -1,4 +0,0 @@ -import Vue from 'vue' -import Antd from 'ant-design-vue' -import 'ant-design-vue/dist/antd.css' -Vue.use(Antd) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js deleted file mode 100644 index 3966b83f..00000000 --- a/frontend/src/router/index.js +++ /dev/null @@ -1,42 +0,0 @@ -import Vue from 'vue' -import Router from 'vue-router' -import HActionView from '@/components/HActionView' -import HTicketList from '@/components/HTicketList' -import HHome from '../components/HHome' -import HTicketDetail from '../components/HTicketDetail' - -Vue.use(Router) - -let router = new Router({ - mode: 'history', - routes: [ - { - path: '/', - name: 'Home', - component: HHome - }, - { - path: '/ticket', - name: 'HTicketList', - component: HTicketList - }, - { - path: '/ticket/:id', - name: 'HTicketDetail', - component: HTicketDetail - }, - { - path: '/ticket/:id/:action', - name: 'HTicketQuickPass', - component: HTicketDetail - }, - { - // Make sure this route is the last, as it can match all route - // See also: https://router.vuejs.org/guide/essentials/dynamic-matching.html#matching-priority - path: '/:name', - name: 'FormView', - component: HActionView - } - ] -}) -export default router diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js deleted file mode 100644 index 190d8370..00000000 --- a/frontend/src/store/index.js +++ /dev/null @@ -1,62 +0,0 @@ -import Vue from 'vue' -import Vuex from 'vuex' -import {getFirstActionFromTree} from '../utils/HFinder' - -Vue.use(Vuex) - -const debug = process.env.NODE_ENV !== 'production' - -export default new Vuex.Store({ - strict: debug, - state: { - userProfile: '', - actionDefinition: '', - actionTree: '' - }, - mutations: { - setUserProfile (state, profile) { - state.userProfile = profile - }, - setActionDefinition (state, definition) { - state.actionDefinition = definition - }, - setActionTree (state, tree) { - state.actionTree = tree - } - }, - actions: { - updateUserProfile ({ commit }, profile) { - commit('setUserProfile', profile) - }, - deleteUserProfile ({ commit }) { - commit('setUserProfile', '') - }, - updateActionDefinition ({ commit }, definition) { - commit('setActionDefinition', definition) - }, - updateActionTree ({ commit }, tree) { - commit('setActionTree', tree) - } - }, - getters: { - isAdmin: (state) => { - try { - return state.userProfile.roles.includes("admin") - } catch { - return false - } - - }, - isAuthenticated: (state) => { - if (state.userProfile) { - if (state.userProfile.is_authenticated) { - return true - } - } - return false - }, - firstAction: (state) => { - return getFirstActionFromTree(state.actionTree) - } - } -}) diff --git a/frontend/src/utils/HRequests.js b/frontend/src/utils/HRequests.js deleted file mode 100644 index 901679cd..00000000 --- a/frontend/src/utils/HRequests.js +++ /dev/null @@ -1,48 +0,0 @@ -import {vm} from '../main.js' -import Vue from "vue"; -const axios = require('axios') -const HRequest = axios.create({ - withCredentials: true -}) - -// let message = vm.$message - -export function tokenInterceptor (instance) { - instance.interceptors.request.use(config => { - config.headers.Authorization = `Bearer ${Vue.prototype.$keycloak.token}` - return config - }, error => { - return Promise.reject(error) - }) -} - -HRequest.interceptors.response.use((response) => { - // Do something with response data - return response -}, function (error) { - // Do something with response error - if (error.response.status === 401) { - // 401, unauthorized , redirect to login page - vm.$message.warning('Login required, redirecting to login page...') - if (vm.$route.name !== 'Login') { - vm.$keycloak.loginFn() - } - } else if (error.response.status === 403) { - // 403, insufficient privillege, Redirect to login page - vm.$message.warning('Insufficient privilege!' + error.response.status + ':' + JSON.stringify(error.response.data)) - if (vm.$route.name !== 'Login') { - vm.$keycloak.loginFn() - } - } else if (error.response.status >= 500) { - vm.$message.error('Internal error, please contact webadmin' + error.response.status + ':' + JSON.stringify(error.response.data)) - } else { - // > 500 internal error, notify only - // > 404 notify only - const rawMsg = JSON.stringify(error.response.data) - const msg = rawMsg.length > 150 ? rawMsg.slice(0, 150) + '...' : rawMsg - vm.$message.warning('Request failed: ' + error.response.status + ':' + msg) - } - return Promise.reject(error) -}) - -export {HRequest} diff --git a/frontend/public/favicon.ico b/frontend/static/favicon.ico similarity index 100% rename from frontend/public/favicon.ico rename to frontend/static/favicon.ico diff --git a/frontend/store/README.md b/frontend/store/README.md new file mode 100644 index 00000000..1972d277 --- /dev/null +++ b/frontend/store/README.md @@ -0,0 +1,10 @@ +# STORE + +**This directory is not required, you can delete it if you don't want to use it.** + +This directory contains your Vuex Store files. +Vuex Store option is implemented in the Nuxt.js framework. + +Creating a file in this directory automatically activates the option in the framework. + +More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). diff --git a/frontend/store/alert.js b/frontend/store/alert.js new file mode 100644 index 00000000..c1928f6d --- /dev/null +++ b/frontend/store/alert.js @@ -0,0 +1,13 @@ + +export const state = () => ({ + content: '', + color: '', + level: '', +}); +export const mutations = { + showMessage(state, payload) { + state.content = payload.content + state.color = payload.color + state.level = payload.level + }, +}; diff --git a/frontend/store/index.js b/frontend/store/index.js new file mode 100644 index 00000000..4016b44a --- /dev/null +++ b/frontend/store/index.js @@ -0,0 +1,56 @@ +import {getFirstActionFromTree} from '@/utils/HFinder' + +export const state = () => ({ + userProfile: '', + actionDefinition: '', + actionTree: '' +}) + +export const mutations = { + setUserProfile (state, profile) { + state.userProfile = profile + }, + setActionDefinition (state, definition) { + state.actionDefinition = definition + }, + setActionTree (state, tree) { + state.actionTree = tree + } +} + +export const actions = { + updateUserProfile ({ commit }, profile) { + commit('setUserProfile', profile) + }, + deleteUserProfile ({ commit }) { + commit('setUserProfile', '') + }, + updateActionDefinition ({ commit }, definition) { + commit('setActionDefinition', definition) + }, + updateActionTree ({ commit }, tree) { + commit('setActionTree', tree) + } +} + +export const getters = { + isAdmin: (state) => { + try { + return state.userProfile.roles.includes("admin") + } catch { + return false + } + + }, + isAuthenticated: (state) => { + if (state.userProfile) { + if (state.userProfile.is_authenticated) { + return true + } + } + return false + }, + firstAction: (state) => { + return getFirstActionFromTree(state.actionTree) + } +} diff --git a/frontend/tests/unit/HFinder.spec.js b/frontend/tests/unit/HFinder.spec.js deleted file mode 100644 index aae80bbb..00000000 --- a/frontend/tests/unit/HFinder.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import {getElementFromArray, getElementsContains} from '@/utils/HFinder' - -describe('FinderTest', function () { - it('testElementFromArray', function () { - let exampleArray = [ - { - key: '1', - title: 'loading', - name: 'test', - url: '/#/' - }] - let findResult = getElementFromArray(exampleArray, 'name', 'test', 'name') - expect(findResult.title).toEqual('loading') - findResult = getElementFromArray(exampleArray, 'name', 'test_does_not_exist') - expect(findResult).toEqual(undefined) - }) - it('testFindInNestedArray', function () { - let a1 = [ - { - key: '11', - title: 'testtitle2', - name: 'test1', - url: '/#/' - }] - let a2 = [ - { - name: 'test3', - children: [{ - name: 'test4' - }] - }, - { - key: '1', - title: 'loading', - name: 'test2', - url: '/#/', - children: a1 - } - ] - let findResult = getElementFromArray(a2, 'name', 'test1') - expect(findResult.title).toBe('testtitle2') - }) -}) - -describe('FilterTest', function () { - it('testElementContains', function () { - let a1 = [ - { - key: '11', - title: 'testtitle2', - name: 'test1', - url: '/#/' - }] - let searchArray = [ - { - name: 'test3', - children: [{ - name: 'test4' - }] - }, - { - key: '1', - title: 'loading', - name: 'test2', - url: '/#/', - children: a1 - } - ] - let filterResult = getElementsContains(searchArray, 'test2') - expect(filterResult.length).toBe(1) - }) -}) diff --git a/frontend/src/utils/HComparer.js b/frontend/utils/HComparer.js similarity index 100% rename from frontend/src/utils/HComparer.js rename to frontend/utils/HComparer.js diff --git a/frontend/src/utils/HDate.js b/frontend/utils/HDate.js similarity index 69% rename from frontend/src/utils/HDate.js rename to frontend/utils/HDate.js index d32b42cf..94930ea9 100644 --- a/frontend/src/utils/HDate.js +++ b/frontend/utils/HDate.js @@ -1,5 +1,5 @@ export function UTCtoLcocalTime (UTCTime) { // Params: UTCTime str - let dateObj = new Date(UTCTime + '+00:00') + const dateObj = new Date(UTCTime + '+00:00') return dateObj.toLocaleString() } diff --git a/frontend/src/utils/HFinder.js b/frontend/utils/HFinder.js similarity index 82% rename from frontend/src/utils/HFinder.js rename to frontend/utils/HFinder.js index 036c01a4..89f19b9b 100644 --- a/frontend/src/utils/HFinder.js +++ b/frontend/utils/HFinder.js @@ -10,7 +10,7 @@ export function getElementFromArray (a, property, value) { return a[i] } if (a[i].children) { - let childFound = getElementFromArray(a[i].children, property, value) + const childFound = getElementFromArray(a[i].children, property, value) if (childFound) { return childFound } @@ -52,15 +52,15 @@ export function addKeyForEachElement (tree, startingKey) { } export function filterArray (a, arrayFilter) { - let result = [] + const result = [] for (let i = 0; i < a.length; i++) { - let isElementValid = arrayFilter(a[i]) + const isElementValid = arrayFilter(a[i]) if (isElementValid) { result.push(Object.assign({}, a[i])) } else if (a[i].children) { - let innerResult = filterArray(a[i].children, arrayFilter) + const innerResult = filterArray(a[i].children, arrayFilter) if (innerResult.length > 0) { - let tempElement = Object.assign({}, a[i]) + const tempElement = Object.assign({}, a[i]) tempElement.children = innerResult result.push(tempElement) } @@ -71,10 +71,10 @@ export function filterArray (a, arrayFilter) { export function getElementsContains (a, s) { return filterArray(a, function (e) { - if (e.name.toLowerCase().indexOf(s.toLowerCase()) !== -1) { + if (e.name.toLowerCase().includes(s.toLowerCase())) { return true } - if (e.target_object && e.target_object.toLowerCase().indexOf(s.toLowerCase()) !== -1) { + if (e.target_object && e.target_object.toLowerCase().includes(s.toLowerCase())) { return true } return false diff --git a/frontend/vue.config.js b/frontend/vue.config.js deleted file mode 100644 index 827ba2d0..00000000 --- a/frontend/vue.config.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - devServer: { - proxy: { - '^/api': { - target: 'http://localhost:8123', // Api server address - changeOrigin: true, - }, - '^/auth': { - target: 'http://localhost:8123', // Api server address - changeOrigin: true - } - } - } -} diff --git a/helpdesk/__init__.py b/helpdesk/__init__.py index f7ddecfb..c27d33e4 100644 --- a/helpdesk/__init__.py +++ b/helpdesk/__init__.py @@ -37,6 +37,8 @@ def create_app(): enabled_middlewares = [ Middleware(ProxyHeadersMiddleware, trusted_hosts=TRUSTED_HOSTS), + Middleware(SessionMiddleware, secret_key=SESSION_SECRET_KEY, max_age=SESSION_TTL), + Middleware(BearerAuthMiddleware), Middleware(CORSMiddleware, allow_origins=ALLOW_ORIGINS, allow_origin_regex=ALLOW_ORIGINS_REG, diff --git a/helpdesk/config.py b/helpdesk/config.py index c67f8d77..f7de8f8c 100644 --- a/helpdesk/config.py +++ b/helpdesk/config.py @@ -76,17 +76,6 @@ SPINCYCLE_USERNAME = '' SPINCYCLE_PASSWORD = '' -OPENID_PROVIDER = { - 'server_metadata_url': "https://fakekeycloak/auth/realms/apps/.well-known/openid-configuration", - 'client_id': 'fakeid' -} - -KEYCLOAK_SETTINGS = { - "url": "https://you_keycloak.com/auth", - "realm": "apps", - "clientId": "helpdesk" -} - try: from local_config import * # NOQA diff --git a/helpdesk/libs/dependency.py b/helpdesk/libs/dependency.py index f1d797c3..66561a2a 100644 --- a/helpdesk/libs/dependency.py +++ b/helpdesk/libs/dependency.py @@ -3,12 +3,8 @@ import logging -import requests - -from authlib.jose import jwt -from authlib.jose.errors import JoseError, ExpiredTokenError, DecodeError from starlette import status -from fastapi import Depends, HTTPException +from fastapi import Depends, HTTPException, Request from fastapi.security import OAuth2PasswordBearer from helpdesk.config import OPENID_PROVIDER, oauth_username_func @@ -17,100 +13,15 @@ logger = logging.getLogger(__name__) -# load auth providers -class Validator: - def __init__(self, metadata_url=None, client_id=None, *args, **kwargs): - self.metadata_url = metadata_url - self.client_id = client_id - if not self.client_id: - raise ValueError('Init validator failed, client_id not set') - self.client_kwargs = kwargs.get('client_kwargs') - self.server_metadata = {} - self.jwk = {} - self.token_endpoint = "" - self.authorization_endpoint = "" - - def fetch_configs(self): - # Fetch the public key for validating Bearer token - self.server_metadata = self.get(self.metadata_url) - self.jwk = self.get(self.server_metadata['jwks_uri']) - self.token_endpoint = self.server_metadata['token_endpoint'] - self.authorization_endpoint = self.server_metadata['authorization_endpoint'] - - def get(self, *args, **kwargs): - if self.client_kwargs: - r = requests.get(*args, **kwargs, **self.client_kwargs) - else: - r = requests.get(*args, **kwargs) - r.raise_for_status() - return r.json() - - def valide_token(self, token: str): - """validate token string, return a parsed token if valid, return None if not valid - :return tuple (is_valid -> bool, id_token or None) - """ - try: - if "https://accounts.google.com" in self.metadata_url: - # google's certs would change from time to time, let's refetch it before every try - self.fetch_configs() - token = jwt.decode(token, self.jwk) - except ValueError as e: - if str(e) == 'Invalid JWK kid': - logger.info( - 'This token cannot be decoded with current provider') - return None, None - else: - raise e - except DecodeError as e: - logger.info("Token decode failed: %s", str(e)) - return False, None - - try: - token.validate() - return True, token - except ExpiredTokenError as e: - logger.info('Auth header expired, %s', e) - return True, None - except JoseError as e: - logger.error('Jose error: %s', e) - return None, None - - def parse_token(self, token: str) -> User or None: - logger.info("Trying to validate token with %s", self.metadata_url) - valid, id_token = self.valide_token(token) - if not id_token: - # not valid - return - # check aud and iss - aud = id_token.get('aud') - if id_token.get('azp') != self.client_id and (not aud or self.client_id not in aud): - logger.info('Token is valid, not expired, but not belonged to this client') - return - username = oauth_username_func(id_token) - email = id_token.get('email', '') - access = id_token.get('resource_access', {}) - app_roles = access.get(self.client_id, {"roles": []}) - - user = User(name=username, email=email, roles=app_roles["roles"], avatar=id_token.get('picture', '')) - - return user - - -registered_validator = Validator(metadata_url=OPENID_PROVIDER['server_metadata_url'], **OPENID_PROVIDER) - - -registered_validator.fetch_configs() -oauth2_scheme = OAuth2PasswordBearer(tokenUrl=registered_validator.token_endpoint) - - -def get_current_user(token: str = Depends(oauth2_scheme)): - user = registered_validator.parse_token(token) - if not user: +def get_current_user(request: Request): + userinfo = request.session.get('user') + if not userinfo: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials", headers={"WWW-Authenticate": "Bearer"}, ) + user = User.parse_raw(userinfo) return user diff --git a/helpdesk/views/auth/index.py b/helpdesk/views/auth/index.py index 7335b953..a5eb6634 100644 --- a/helpdesk/views/auth/index.py +++ b/helpdesk/views/auth/index.py @@ -6,7 +6,7 @@ from fastapi import Request -from helpdesk.config import OPENID_PRIVIDERS, oauth_username_func, KEYCLOAK_SETTINGS +from helpdesk.config import OPENID_PRIVIDERS, oauth_username_func, KEYCLOAK_SETTINGS, DEFAULT_BASE_URL from helpdesk.models.user import User from . import router @@ -22,47 +22,36 @@ oauth_clients[provider] = client -@router.get("/oidc-configs.json") -async def oidc_configs() -> dict: - return KEYCLOAK_SETTINGS +@router.get('/oauth/{oauth_provider}') +async def oauth(request: Request): - -@router.get('/oauth/{provider}') -async def oauth(request): - - provider = request.path_params.get('provider', '') - client = oauth_clients[provider] + oauth_provider = request.path_params.get('oauth_provider', '') + oauth_client = oauth_clients[oauth_provider] # FIXME: url_for behind proxy - url_path = request['router'].url_path_for('auth:callback', provider=provider) - server = request["server"] - if server[1] in (80, 443): - base_url = f"{request['scheme']}://{server[0]}{request.get('app_root_path', '/')}" - else: - base_url = f"{request['scheme']}://{server[0]}:{server[1]}{request.get('app_root_path', '/')}" - redirect_uri = url_path.make_absolute_url(base_url=base_url) + url_path = request.app.router.url_path_for('callback', oauth_provider=oauth_provider) + redirect_uri = url_path.make_absolute_url(base_url=DEFAULT_BASE_URL) - return await client.authorize_redirect(request, redirect_uri) + return await oauth_client.authorize_redirect(request, redirect_uri) -@router.get('/callback/{provider}') -async def callback(request): - provider = request.path_params.get('provider', '') - client = oauth_clients[provider] +@router.get('/callback/{oauth_provider}') +async def callback(oauth_provider: str, request: Request): + oauth_client = oauth_clients[oauth_provider] - token = await client.authorize_access_token(request) - id_token = await client.parse_id_token(request, token) + token = await oauth_client.authorize_access_token(request) + id_token = await oauth_client.parse_id_token(request, token) logger.debug("auth succeed %s", id_token) username = oauth_username_func(id_token) email = id_token['email'] access = id_token.get('resource_access', {}) - roles = access.get(client.client_id, {}).get('roles', []) + roles = access.get(oauth_client.client_id, {}).get('roles', []) - user = User(username, email, roles, id_token.get('picture', '')) + user = User(name=username, email=email, roles=roles, avatar=id_token.get('picture')) - request.session['user'] = user.to_json() + request.session['user'] = user.json() return HTMLResponse("OK, you can close this window now", 200) diff --git a/requirements.txt b/requirements.txt index 930321dd..986d4a5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,6 @@ mysqlclient>=1.4.2 cached-property>=1.5.1 st2client==3.3.0 rule==0.1.1 -Authlib==0.15.2 +Authlib==0.* httpx==0.* fastapi==0.*