From f5e8698e572ef4f84bc3277cd9751806f21b750e Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 16 Oct 2019 11:04:33 +0800 Subject: [PATCH] user management for the webui: - some UI elements are hidden for non-admin users - server settings screen to administrate users - account settings screen to update user's password --- komga-webui/package-lock.json | 5 + komga-webui/package.json | 3 +- .../src/components/AccountSettings.vue | 59 ++++++ komga-webui/src/components/AddUserDialog.vue | 174 ++++++++++++++++++ .../src/components/DeleteUserDialog.vue | 111 +++++++++++ .../src/components/PasswordChangeDialog.vue | 167 +++++++++++++++++ komga-webui/src/components/SettingsUsers.vue | 90 +++++++++ komga-webui/src/components/Welcome.vue | 9 +- komga-webui/src/main.ts | 5 + komga-webui/src/plugins/komga-users.plugin.ts | 60 ++++++ komga-webui/src/router.ts | 40 +++- .../src/services/komga-filesystem.service.ts | 18 +- .../src/services/komga-users.service.ts | 71 +++++++ komga-webui/src/types/komga-users.ts | 14 ++ komga-webui/src/views/Home.vue | 60 ++++-- 15 files changed, 863 insertions(+), 23 deletions(-) create mode 100644 komga-webui/src/components/AccountSettings.vue create mode 100644 komga-webui/src/components/AddUserDialog.vue create mode 100644 komga-webui/src/components/DeleteUserDialog.vue create mode 100644 komga-webui/src/components/PasswordChangeDialog.vue create mode 100644 komga-webui/src/components/SettingsUsers.vue create mode 100644 komga-webui/src/plugins/komga-users.plugin.ts create mode 100644 komga-webui/src/services/komga-users.service.ts create mode 100644 komga-webui/src/types/komga-users.ts diff --git a/komga-webui/package-lock.json b/komga-webui/package-lock.json index ef9ae727f..8d806d2c1 100644 --- a/komga-webui/package-lock.json +++ b/komga-webui/package-lock.json @@ -15577,6 +15577,11 @@ "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.1.tgz", "integrity": "sha512-ER5moSbLZuNSMBFnEBVGhQ1uCBNJslH9W/Dw2W7GZN23UQA69uapP5GTT9Vm8Trc0PzBSVt6LzF3hGjmv41xcg==" }, + "vuex-router-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vuex-router-sync/-/vuex-router-sync-5.0.0.tgz", + "integrity": "sha512-Mry2sO4kiAG64714X1CFpTA/shUH1DmkZ26DFDtwoM/yyx6OtMrc+MxrU+7vvbNLO9LSpgwkiJ8W+rlmRtsM+w==" + }, "w3c-hr-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", diff --git a/komga-webui/package.json b/komga-webui/package.json index 8d420fee3..52cc7b6fe 100644 --- a/komga-webui/package.json +++ b/komga-webui/package.json @@ -17,7 +17,8 @@ "vue-router": "^3.0.3", "vuelidate": "^0.7.4", "vuetify": "^2.1.2", - "vuex": "^3.0.1" + "vuex": "^3.0.1", + "vuex-router-sync": "^5.0.0" }, "devDependencies": { "@types/jest": "^23.1.4", diff --git a/komga-webui/src/components/AccountSettings.vue b/komga-webui/src/components/AccountSettings.vue new file mode 100644 index 000000000..84e701ad5 --- /dev/null +++ b/komga-webui/src/components/AccountSettings.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/komga-webui/src/components/AddUserDialog.vue b/komga-webui/src/components/AddUserDialog.vue new file mode 100644 index 000000000..fa56af779 --- /dev/null +++ b/komga-webui/src/components/AddUserDialog.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/komga-webui/src/components/DeleteUserDialog.vue b/komga-webui/src/components/DeleteUserDialog.vue new file mode 100644 index 000000000..30373d4c6 --- /dev/null +++ b/komga-webui/src/components/DeleteUserDialog.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/komga-webui/src/components/PasswordChangeDialog.vue b/komga-webui/src/components/PasswordChangeDialog.vue new file mode 100644 index 000000000..c770f279b --- /dev/null +++ b/komga-webui/src/components/PasswordChangeDialog.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/komga-webui/src/components/SettingsUsers.vue b/komga-webui/src/components/SettingsUsers.vue new file mode 100644 index 000000000..02bceafb7 --- /dev/null +++ b/komga-webui/src/components/SettingsUsers.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/komga-webui/src/components/Welcome.vue b/komga-webui/src/components/Welcome.vue index 527d35dcc..98ffb1637 100644 --- a/komga-webui/src/components/Welcome.vue +++ b/komga-webui/src/components/Welcome.vue @@ -9,7 +9,7 @@

Welcome to Komga

The user interface is quite new, more features will come in future releases!

- Add library + Add library
@@ -19,7 +19,12 @@ import Vue from 'vue' export default Vue.extend({ - name: 'Welcome' + name: 'Welcome', + computed: { + isAdmin (): boolean { + return this.$store.getters.meAdmin + } + } }) diff --git a/komga-webui/src/main.ts b/komga-webui/src/main.ts index 78d1f7eb6..4cc109115 100644 --- a/komga-webui/src/main.ts +++ b/komga-webui/src/main.ts @@ -1,9 +1,11 @@ import Vue from 'vue' import Vuelidate from 'vuelidate' +import { sync } from 'vuex-router-sync' import App from './App.vue' import httpPlugin from './plugins/http.plugin' import komgaFileSystem from './plugins/komga-filesystem.plugin' import komgaLibraries from './plugins/komga-libraries.plugin' +import komgaUsers from './plugins/komga-users.plugin' import vuetify from './plugins/vuetify' import router from './router' import store from './store' @@ -12,10 +14,13 @@ Vue.use(Vuelidate) Vue.use(httpPlugin) Vue.use(komgaFileSystem, { http: Vue.prototype.$http }) +Vue.use(komgaUsers, { store: store, http: Vue.prototype.$http }) Vue.use(komgaLibraries, { store: store, http: Vue.prototype.$http }) Vue.config.productionTip = false +sync(store, router) + new Vue({ router, store, diff --git a/komga-webui/src/plugins/komga-users.plugin.ts b/komga-webui/src/plugins/komga-users.plugin.ts new file mode 100644 index 000000000..163944235 --- /dev/null +++ b/komga-webui/src/plugins/komga-users.plugin.ts @@ -0,0 +1,60 @@ +import KomgaUsersService from '@/services/komga-users.service' +import { AxiosInstance } from 'axios' +import _Vue from 'vue' +import { Module } from 'vuex/types' + +let service: KomgaUsersService + +const vuexModule: Module = { + state: { + me: {} as UserDto, + users: [] as UserDto[] + }, + getters: { + meAdmin: state => state.me.hasOwnProperty('roles') && state.me.roles.includes('ADMIN') + }, + mutations: { + setMe (state, user: UserDto) { + state.me = user + }, + setAllUsers (state, users: UserDto[]) { + state.users = users + } + }, + actions: { + async getMe ({ commit }) { + commit('setMe', await service.getMe()) + }, + async updateMyPassword (_, newPassword: PasswordUpdateDto) { + await service.patchMePassword(newPassword) + }, + async getAllUsers ({ commit }) { + commit('setAllUsers', await service.getAll()) + }, + async postUser ({ dispatch }, user: UserCreationDto) { + await service.postUser(user) + dispatch('getAllUsers') + }, + async deleteUser ({ dispatch }, user: UserDto) { + await service.deleteUser(user) + dispatch('getAllUsers') + } + } +} + +export default { + install ( + Vue: typeof _Vue, + { store, http }: { store: any, http: AxiosInstance }) { + service = new KomgaUsersService(http) + Vue.prototype.$komgaUsers = service + + store.registerModule('komgaUsers', vuexModule) + } +} + +declare module 'vue/types/vue' { + interface Vue { + $$komgaUsers: KomgaUsersService; + } +} diff --git a/komga-webui/src/router.ts b/komga-webui/src/router.ts index 5221841c8..88166294c 100644 --- a/komga-webui/src/router.ts +++ b/komga-webui/src/router.ts @@ -1,34 +1,68 @@ import Vue from 'vue' import Router from 'vue-router' +import store from './store' Vue.use(Router) +const lStore = store + +const adminGuard = (to: any, from: any, next: any) => { + if (!lStore.getters.meAdmin) next({ name: 'home' }) + else next() +} + export default new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', - redirect: { name: 'welcome' }, name: 'home', + redirect: { name: 'welcome' }, component: () => import(/* webpackChunkName: "home" */ './views/Home.vue'), children: [ { path: '/libraries/add', name: 'addlibrary', + beforeEnter: adminGuard, component: () => import(/* webpackChunkName: "addlibrary" */ './components/AddLibraryDialog.vue') }, { path: '/welcome', name: 'welcome', component: () => import(/* webpackChunkName: "welcome" */ './components/Welcome.vue') + }, + { + path: '/settings', + name: 'settings', + redirect: { name: 'settings-users' } + }, + { + path: '/settings/users', + name: 'settings-users', + beforeEnter: adminGuard, + component: () => import(/* webpackChunkName: "settings-users" */ './components/SettingsUsers.vue'), + children: [ + { + path: '/settings/users/add', + name: 'settings-users-add', + component: () => import(/* webpackChunkName: "settings-user" */ './components/AddUserDialog.vue') + } + ] + }, + { + path: '/account', + name: 'account', + component: () => import(/* webpackChunkName: "account" */ './components/AccountSettings.vue') } ] }, { path: '*', - name: 'notfound', - component: () => import(/* webpackChunkName: "notfound" */ './views/PageNotFound.vue') + name: + 'notfound', + component: + () => import(/* webpackChunkName: "notfound" */ './views/PageNotFound.vue') } ] }) diff --git a/komga-webui/src/services/komga-filesystem.service.ts b/komga-webui/src/services/komga-filesystem.service.ts index a247df908..652ef0bd2 100644 --- a/komga-webui/src/services/komga-filesystem.service.ts +++ b/komga-webui/src/services/komga-filesystem.service.ts @@ -10,10 +10,20 @@ export default class KomgaFilesystemService { } async getDirectoryListing (path: String = ''): Promise { - return (await this.http.get(API_FILESYSTEM, { - params: { - path: path + try { + return (await this.http.get(API_FILESYSTEM, + { + params: { + path: path + } + } + )).data + } catch (e) { + let msg = 'An error occurred while trying to retrieve directory listing' + if (e.response.data.message) { + msg += `: ${e.response.data.message}` } - })).data + throw new Error(msg) + } } } diff --git a/komga-webui/src/services/komga-users.service.ts b/komga-webui/src/services/komga-users.service.ts new file mode 100644 index 000000000..8ce6d23d2 --- /dev/null +++ b/komga-webui/src/services/komga-users.service.ts @@ -0,0 +1,71 @@ +import { AxiosInstance } from 'axios' + +const API_USERS = '/api/v1/users' + +export default class KomgaUsersService { + private http: AxiosInstance; + + constructor (http: AxiosInstance) { + this.http = http + } + + async getMe (): Promise { + try { + return (await this.http.get(`${API_USERS}/me`)).data + } catch (e) { + let msg = 'An error occurred while trying to retrieve current user' + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async getAll (): Promise { + try { + return (await this.http.get(`${API_USERS}`)).data + } catch (e) { + let msg = 'An error occurred while trying to retrieve all users' + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async postUser (user: UserCreationDto): Promise { + try { + return (await this.http.post(API_USERS, user)).data + } catch (e) { + let msg = `An error occurred while trying to add user '${user.email}'` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async deleteUser (user: UserDto) { + try { + await this.http.delete(`${API_USERS}/${user.id}`) + } catch (e) { + let msg = `An error occurred while trying to delete user '${user.email}'` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async patchMePassword (newPassword: PasswordUpdateDto) { + try { + return (await this.http.patch(`${API_USERS}/me/password`, newPassword)).data + } catch (e) { + let msg = `An error occurred while trying to update password for current user` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } +} diff --git a/komga-webui/src/types/komga-users.ts b/komga-webui/src/types/komga-users.ts new file mode 100644 index 000000000..e9124b591 --- /dev/null +++ b/komga-webui/src/types/komga-users.ts @@ -0,0 +1,14 @@ +interface UserDto { + id: number, + email: string, + roles: string[] +} + +interface UserCreationDto { + email: string, + roles: string[] +} + +interface PasswordUpdateDto { + password: string +} diff --git a/komga-webui/src/views/Home.vue b/komga-webui/src/views/Home.vue index babe47fe6..9b4645b6c 100644 --- a/komga-webui/src/views/Home.vue +++ b/komga-webui/src/views/Home.vue @@ -5,6 +5,14 @@ hide-on-scroll > + + + {{ t.name }} + + @@ -39,37 +47,48 @@ Libraries - - mdi-plus - + + + mdi-plus + + - + {{ l.root }} - + mdi-delete - - - - - - - - + + + mdi-settings + + + Server settings + + + + + + mdi-account + + + Account settings + + @@ -126,10 +145,25 @@ export default Vue.extend({ computed: { libraries (): LibraryDto[] { return this.$store.state.komgaLibraries.libraries + }, + isAdmin (): boolean { + return this.$store.getters.meAdmin + }, + tabs () { + if (this.$store.state.route.name) { + if (this.$store.state.route.name.startsWith('settings')) { + return [ + { id: 'tab-users', route: 'settings-users', name: 'Users' } + ] + } + return [] + } + return [] } }, async mounted () { try { + await this.$store.dispatch('getMe') await this.$store.dispatch('getLibraries') } catch (e) { this.showSnack(e.message)