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 @@
+
+
+
+ Account Settings
+
+
+ Email
+
+
+
+
+
+ Roles
+
+
+ {{ role }}
+
+
+
+
+
+ Change password
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ mdi-close
+
+ {{ dialogTitle }}
+
+
+ {{ confirmText }}
+
+
+
+ {{ dialogTitle }}
+
+
+
+
+
+
+
+
+
+ Cancel
+ {{ confirmText }}
+
+
+
+
+
+ {{ snackText }}
+
+ Close
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ Delete User
+
+
+
+
+ The user {{ user.email }} will be deleted from this server. This cannot be undone.
+ Continue ?
+
+
+
+
+
+
+
+ Yes, delete the user "{{ user.email }}"
+
+
+
+
+
+
+
+
+
+ Cancel
+ Delete
+
+
+
+
+
+
+ {{ snackText }}
+
+ Close
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ Change password
+
+
+
+
+
+
+
+ Cancel
+ Change password
+
+
+
+
+
+
+ {{ snackText }}
+
+ Close
+
+
+
+
+
+
+
+
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 @@
+
+
+
+ Users
+
+
+
+
+
+
+
+
+
+
+ mdi-account-star
+ mdi-account
+
+
+ {{ u.roles.includes('ADMIN') ? 'Administrator' : 'User' }}
+
+
+
+
+ {{ u.email }}
+
+
+
+
+
+ mdi-delete
+
+
+
+
+
+
+
+
+
+ mdi-plus
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.name }}
{{ 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)