mirror of
https://github.com/gotson/komga.git
synced 2026-05-09 05:10:19 +02:00
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
This commit is contained in:
parent
76521fe111
commit
f5e8698e57
15 changed files with 863 additions and 23 deletions
5
komga-webui/package-lock.json
generated
5
komga-webui/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
59
komga-webui/src/components/AccountSettings.vue
Normal file
59
komga-webui/src/components/AccountSettings.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<span class="headline">Account Settings</span>
|
||||
</v-row>
|
||||
<v-row align="center">
|
||||
<v-col cols="2">Email</v-col>
|
||||
<v-col>
|
||||
<v-text-field readonly v-model="me.email"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row align="center">
|
||||
<v-col cols="2">Roles</v-col>
|
||||
<v-col>
|
||||
<v-chip-group>
|
||||
<v-chip v-for="role in me.roles" :key="role">{{ role }}</v-chip>
|
||||
</v-chip-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn color="primary"
|
||||
@click.prevent="modalPasswordChange = true"
|
||||
>Change password
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<password-change-dialog v-model="modalPasswordChange"
|
||||
:user="me"
|
||||
></password-change-dialog>
|
||||
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import PasswordChangeDialog from '@/components/PasswordChangeDialog.vue'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'AccountSettings',
|
||||
components: { PasswordChangeDialog },
|
||||
data: () => {
|
||||
return {
|
||||
modalPasswordChange: false,
|
||||
newPassword: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
me (): UserDto {
|
||||
return this.$store.state.komgaUsers.me
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
174
komga-webui/src/components/AddUserDialog.vue
Normal file
174
komga-webui/src/components/AddUserDialog.vue
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="modalAddUser"
|
||||
:fullscreen="this.$vuetify.breakpoint.xsOnly"
|
||||
:hide-overlay="this.$vuetify.breakpoint.xsOnly"
|
||||
max-width="450"
|
||||
>
|
||||
<v-card>
|
||||
<v-toolbar class="hidden-sm-and-up">
|
||||
<v-btn icon @click="dialogCancel">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>{{ dialogTitle }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items>
|
||||
<v-btn text color="primary" @click="dialogConfirm">{{ confirmText }}</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-title class="hidden-xs-only">{{ dialogTitle }}</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
|
||||
<form novalidate>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="form.email"
|
||||
label="Email"
|
||||
:error-messages="getErrors('email')"
|
||||
@blur="$v.form.email.$touch()"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="form.password"
|
||||
label="Password"
|
||||
autocomplete="off"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
:error-messages="getErrors('password')"
|
||||
@input="$v.form.password.$touch()"
|
||||
@blur="$v.form.password.$touch()"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<p>Roles</p>
|
||||
<v-checkbox
|
||||
v-model="form.admin"
|
||||
label="Administrator"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</form>
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="hidden-xs-only">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="dialogCancel">Cancel</v-btn>
|
||||
<v-btn text class="primary--text" @click="dialogConfirm">{{ confirmText }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
bottom
|
||||
color="error"
|
||||
>
|
||||
{{ snackText }}
|
||||
<v-btn
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Vue from 'vue'
|
||||
import { email, required } from 'vuelidate/lib/validators'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'AddUserDialog',
|
||||
data: () => {
|
||||
return {
|
||||
modalAddUser: true,
|
||||
showPassword: false,
|
||||
snackbar: false,
|
||||
snackText: '',
|
||||
dialogTitle: 'Add User',
|
||||
confirmText: 'Add',
|
||||
form: {
|
||||
email: '',
|
||||
password: '',
|
||||
admin: false
|
||||
},
|
||||
validationFieldNames: new Map([])
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modalAddUser (val) {
|
||||
!val && this.dialogCancel()
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
form: {
|
||||
email: { required, email },
|
||||
password: { required }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getErrors (fieldName) {
|
||||
const errors = []
|
||||
|
||||
const field = this.$v.form[fieldName]
|
||||
if (field && field.$invalid && field.$dirty) {
|
||||
const properName = this.validationFieldNames.has(fieldName)
|
||||
? this.validationFieldNames.get(fieldName) : fieldName.charAt(0).toUpperCase() + fieldName.substring(1)
|
||||
if (!field.required) errors.push(`${properName} is required.`)
|
||||
if (!field.email) errors.push(`${properName} must be a valid email address.`)
|
||||
}
|
||||
return errors
|
||||
},
|
||||
showSnack (message) {
|
||||
this.snackText = message
|
||||
this.snackbar = true
|
||||
},
|
||||
dialogCancel () {
|
||||
this.$router.push({ name: 'settings-users' })
|
||||
},
|
||||
dialogConfirm () {
|
||||
this.addUser()
|
||||
},
|
||||
validateUser () {
|
||||
this.$v.$touch()
|
||||
|
||||
if (!this.$v.$invalid) {
|
||||
return {
|
||||
email: this.form.email,
|
||||
password: this.form.password,
|
||||
roles: this.form.admin ? ['ADMIN'] : []
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
async addUser () {
|
||||
const user = this.validateUser()
|
||||
if (user) {
|
||||
try {
|
||||
await this.$store.dispatch('postUser', user)
|
||||
this.$router.push({ name: 'settings-users' })
|
||||
} catch (e) {
|
||||
this.showSnack(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
111
komga-webui/src/components/DeleteUserDialog.vue
Normal file
111
komga-webui/src/components/DeleteUserDialog.vue
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="450"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>Delete User</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>The user <b>{{ user.email }}</b> will be deleted from this server. This <b>cannot</b> be undone.
|
||||
Continue ?
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-checkbox v-model="confirmDelete" color="red">
|
||||
<template v-slot:label>
|
||||
Yes, delete the user "{{ user.email }}"
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="dialogCancel">Cancel</v-btn>
|
||||
<v-btn text class="red--text"
|
||||
@click="dialogConfirm"
|
||||
:disabled="!confirmDelete"
|
||||
>Delete
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
bottom
|
||||
color="error"
|
||||
>
|
||||
{{ snackText }}
|
||||
<v-btn
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'DeleteUserDialog',
|
||||
data: () => {
|
||||
return {
|
||||
confirmDelete: false,
|
||||
snackbar: false,
|
||||
snackText: '',
|
||||
modal: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
user: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (val) {
|
||||
this.modal = val
|
||||
},
|
||||
modal (val) {
|
||||
!val && this.dialogCancel()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dialogCancel () {
|
||||
this.$emit('input', false)
|
||||
this.confirmDelete = false
|
||||
},
|
||||
dialogConfirm () {
|
||||
this.deleteUser()
|
||||
this.$emit('input', false)
|
||||
},
|
||||
showSnack (message: string) {
|
||||
this.snackText = message
|
||||
this.snackbar = true
|
||||
},
|
||||
async deleteUser () {
|
||||
try {
|
||||
await this.$store.dispatch('deleteUser', this.user)
|
||||
} catch (e) {
|
||||
this.showSnack(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
167
komga-webui/src/components/PasswordChangeDialog.vue
Normal file
167
komga-webui/src/components/PasswordChangeDialog.vue
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="450"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>Change password</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<form novalidate>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field label="New password"
|
||||
v-model="form.newPassword"
|
||||
autocomplete="off"
|
||||
:type="showPassword1 ? 'text' : 'password'"
|
||||
:append-icon="showPassword1 ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword1 = !showPassword1"
|
||||
:error-messages="getErrors('newPassword')"
|
||||
@input="$v.form.newPassword.$touch()"
|
||||
@blur="$v.form.newPassword.$touch()"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field label="Repeat new password"
|
||||
v-model="form.repeatPassword"
|
||||
autocomplete="off"
|
||||
:type="showPassword2 ? 'text' : 'password'"
|
||||
:append-icon="showPassword2 ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword2 = !showPassword2"
|
||||
:error-messages="getErrors('repeatPassword')"
|
||||
@input="$v.form.repeatPassword.$touch()"
|
||||
@blur="$v.form.repeatPassword.$touch()"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="dialogCancel">Cancel</v-btn>
|
||||
<v-btn text class="primary--text"
|
||||
@click="dialogConfirm"
|
||||
>Change password
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
bottom
|
||||
color="error"
|
||||
>
|
||||
{{ snackText }}
|
||||
<v-btn
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Vue from 'vue'
|
||||
import { required, sameAs } from 'vuelidate/lib/validators'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'PasswordChangeDialog',
|
||||
data: () => {
|
||||
return {
|
||||
modal: false,
|
||||
showPassword1: false,
|
||||
showPassword2: false,
|
||||
snackbar: false,
|
||||
snackText: '',
|
||||
form: {
|
||||
newPassword: '',
|
||||
repeatPassword: ''
|
||||
},
|
||||
validationFieldNames: new Map([
|
||||
['newPassword', 'Password'],
|
||||
['confirmPassword', 'Password confirmation']
|
||||
])
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
user: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (val) {
|
||||
this.modal = val
|
||||
},
|
||||
modal (val) {
|
||||
!val && this.dialogCancel()
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
form: {
|
||||
newPassword: { required },
|
||||
repeatPassword: { sameAsPassword: sameAs('newPassword') }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getErrors (fieldName) {
|
||||
const errors = []
|
||||
const field = this.$v.form[fieldName]
|
||||
if (field && field.$invalid && field.$dirty) {
|
||||
if (fieldName === 'newPassword') {
|
||||
if (!field.required) errors.push(`New password is required.`)
|
||||
}
|
||||
if (fieldName === 'repeatPassword') {
|
||||
if (!field.sameAsPassword) errors.push(`Passwords must be identical.`)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
},
|
||||
showSnack (message) {
|
||||
this.snackText = message
|
||||
this.snackbar = true
|
||||
},
|
||||
formReset () {
|
||||
this.form.newPassword = ''
|
||||
this.form.repeatPassword = ''
|
||||
this.showPassword1 = false
|
||||
this.showPassword2 = false
|
||||
this.$v.$reset()
|
||||
},
|
||||
dialogCancel () {
|
||||
this.$emit('input', false)
|
||||
this.formReset()
|
||||
},
|
||||
dialogConfirm () {
|
||||
this.$v.$touch()
|
||||
|
||||
if (!this.$v.$invalid) {
|
||||
this.updatePassword()
|
||||
this.$emit('input', false)
|
||||
this.formReset()
|
||||
}
|
||||
},
|
||||
async updatePassword () {
|
||||
try {
|
||||
await this.$store.dispatch('updateMyPassword', { password: this.form.newPassword })
|
||||
} catch (e) {
|
||||
this.showSnack(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
90
komga-webui/src/components/SettingsUsers.vue
Normal file
90
komga-webui/src/components/SettingsUsers.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<span class="headline">Users</span>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4" lg="4" xl="4">
|
||||
<div style="position: relative">
|
||||
<v-list
|
||||
elevation="3"
|
||||
>
|
||||
<div v-for="(u, index) in users" :key="index">
|
||||
<v-list-item
|
||||
>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-list-item-icon v-on="on">
|
||||
<v-icon v-if="u.roles.includes('ADMIN')" color="red">mdi-account-star</v-icon>
|
||||
<v-icon v-else>mdi-account</v-icon>
|
||||
</v-list-item-icon>
|
||||
</template>
|
||||
<span>{{ u.roles.includes('ADMIN') ? 'Administrator' : 'User' }}</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ u.email }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-action>
|
||||
<v-btn icon @click="promptDeleteUser(u)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="index !== users.length-1"></v-divider>
|
||||
</div>
|
||||
</v-list>
|
||||
|
||||
<v-btn fab absolute bottom right color="primary"
|
||||
class="mr-6"
|
||||
:to="{name: 'settings-users-add'}">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<delete-user-dialog v-model="modalDeleteUser"
|
||||
:user="userToDelete">
|
||||
</delete-user-dialog>
|
||||
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import DeleteUserDialog from '@/components/DeleteUserDialog.vue'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SettingsUsers',
|
||||
components: { DeleteUserDialog },
|
||||
data: () => ({
|
||||
modalDeleteUser: false,
|
||||
modalAddUser: false,
|
||||
userToDelete: {} as UserDto
|
||||
}),
|
||||
computed: {
|
||||
users (): UserDto[] {
|
||||
return this.$store.state.komgaUsers.users
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
await this.$store.dispatch('getAllUsers')
|
||||
},
|
||||
methods: {
|
||||
promptDeleteUser (user: UserDto) {
|
||||
this.userToDelete = user
|
||||
this.modalDeleteUser = true
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<div class="text-center">
|
||||
<h1 class="headline mt-4">Welcome to Komga</h1>
|
||||
<p class="body-1">The user interface is quite new, more features will come in future releases!</p>
|
||||
<v-btn color="primary" :to="{name: 'addlibrary'}">Add library</v-btn>
|
||||
<v-btn color="primary" :to="{name: 'addlibrary'}" v-if="isAdmin">Add library</v-btn>
|
||||
</div>
|
||||
</v-row>
|
||||
</div>
|
||||
|
|
@ -19,7 +19,12 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'Welcome'
|
||||
name: 'Welcome',
|
||||
computed: {
|
||||
isAdmin (): boolean {
|
||||
return this.$store.getters.meAdmin
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
60
komga-webui/src/plugins/komga-users.plugin.ts
Normal file
60
komga-webui/src/plugins/komga-users.plugin.ts
Normal file
|
|
@ -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<any, any> = {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,10 +10,20 @@ export default class KomgaFilesystemService {
|
|||
}
|
||||
|
||||
async getDirectoryListing (path: String = ''): Promise<DirectoryListingDto> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
komga-webui/src/services/komga-users.service.ts
Normal file
71
komga-webui/src/services/komga-users.service.ts
Normal file
|
|
@ -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<UserDto> {
|
||||
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<UserDto[]> {
|
||||
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<UserDto> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
komga-webui/src/types/komga-users.ts
Normal file
14
komga-webui/src/types/komga-users.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
interface UserDto {
|
||||
id: number,
|
||||
email: string,
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
interface UserCreationDto {
|
||||
email: string,
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
interface PasswordUpdateDto {
|
||||
password: string
|
||||
}
|
||||
|
|
@ -5,6 +5,14 @@
|
|||
hide-on-scroll
|
||||
>
|
||||
<v-app-bar-nav-icon @click.stop="toggleDrawer"></v-app-bar-nav-icon>
|
||||
|
||||
<v-tabs v-if="tabs.length > 0">
|
||||
<v-tab v-for="(t, index) in tabs" :key="index"
|
||||
:id="t.id"
|
||||
:to="{name: t.route}"
|
||||
>{{ t.name }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-app-bar>
|
||||
|
||||
<v-navigation-drawer app v-model="drawerVisible">
|
||||
|
|
@ -39,37 +47,48 @@
|
|||
<v-list-item-content>
|
||||
<v-list-item-title>Libraries</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-btn icon :to="{name: 'addlibrary'}" exact>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
<v-list-item-action v-if="isAdmin">
|
||||
<v-btn icon :to="{name: 'addlibrary'}" exact>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-for="(l, index) in libraries" :key="index" dense>
|
||||
<v-list-item-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-tooltip bottom>
|
||||
<v-tooltip bottom :disabled="!isAdmin">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-list-item-title v-on="on">{{ l.name }}</v-list-item-title>
|
||||
</template>
|
||||
<span>{{ l.root }}</span>
|
||||
</v-tooltip>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-list-item-action v-if="isAdmin">
|
||||
<v-btn icon @click="promptDeleteLibrary(l)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
<!-- <v-list-item :to="{name: 'settings'}">-->
|
||||
<!-- <v-list-item-action>-->
|
||||
<!-- <v-icon>mdi-settings</v-icon>-->
|
||||
<!-- </v-list-item-action>-->
|
||||
<!-- <v-list-item-content>-->
|
||||
<!-- <v-list-item-title>Settings</v-list-item-title>-->
|
||||
<!-- </v-list-item-content>-->
|
||||
<!-- </v-list-item>-->
|
||||
<v-list-item :to="{name: 'settings'}" v-if="isAdmin">
|
||||
<v-list-item-action>
|
||||
<v-icon>mdi-settings</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Server settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item :to="{name: 'account'}">
|
||||
<v-list-item-action>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Account settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<!-- <v-list-item @click="logout">-->
|
||||
<!-- <v-list-item-icon>-->
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue