feat(webui): restrict page streaming and file download per user

also add the ability to edit user roles

closes #146
This commit is contained in:
Gauthier Roebroeck 2020-06-10 14:56:52 +08:00
parent 6291dab864
commit 381b196033
9 changed files with 237 additions and 16 deletions

View file

@ -45,7 +45,7 @@
<!-- FAB reading (center) -->
<v-btn
v-if="bookReady && !selected && !preselect"
v-if="bookReady && !selected && !preselect && canReadPages"
fab
x-large
color="accent"
@ -144,8 +144,11 @@ export default Vue.extend({
return {}
},
computed: {
canReadPages (): boolean {
return this.$store.getters.mePageStreaming
},
overlay (): boolean {
return this.onEdit !== undefined || this.onSelected !== undefined || this.bookReady
return this.onEdit !== undefined || this.onSelected !== undefined || this.bookReady || this.canReadPages
},
computedItem (): Item<BookDto | SeriesDto> {
return createItem(this.item)

View file

@ -52,8 +52,22 @@
<v-col>
<span>Roles</span>
<v-checkbox
v-model="form.admin"
v-model="form.roles"
label="Administrator"
:value="UserRoles.ADMIN"
hide-details
/>
<v-checkbox
v-model="form.roles"
label="Page Streaming"
:value="UserRoles.PAGE_STREAMING"
hide-details
/>
<v-checkbox
v-model="form.roles"
label="File Download"
:value="UserRoles.FILE_DOWNLOAD"
hide-details
/>
</v-col>
</v-row>
@ -87,6 +101,7 @@
</template>
<script lang="ts">
import { UserRoles } from '@/types/enum-users'
import Vue from 'vue'
import { email, required } from 'vuelidate/lib/validators'
@ -94,6 +109,7 @@ export default Vue.extend({
name: 'UserAddDialog',
data: () => {
return {
UserRoles,
modalAddUser: true,
showPassword: false,
snackbar: false,
@ -103,7 +119,7 @@ export default Vue.extend({
form: {
email: '',
password: '',
admin: false,
roles: [UserRoles.PAGE_STREAMING, UserRoles.FILE_DOWNLOAD],
},
validationFieldNames: new Map([]),
}
@ -149,7 +165,7 @@ export default Vue.extend({
return {
email: this.form.email,
password: this.form.password,
roles: this.form.admin ? ['ADMIN'] : [],
roles: this.form.roles,
}
}
return null

View file

@ -0,0 +1,142 @@
<template>
<div>
<v-dialog v-model="modal"
max-width="450"
>
<v-card>
<v-card-title>Edit user</v-card-title>
<v-card-text>
<v-container fluid>
<v-row>
<v-col>
<span class="subtitle-1">Roles for {{ user.email }}</span>
</v-col>
</v-row>
<v-row>
<v-col>
<v-checkbox
v-model="roles"
label="Administrator"
:value="UserRoles.ADMIN"
hide-details
/>
<v-checkbox
v-model="roles"
label="Page Streaming"
:value="UserRoles.PAGE_STREAMING"
hide-details
/>
<v-checkbox
v-model="roles"
label="File Download"
:value="UserRoles.FILE_DOWNLOAD"
hide-details
/>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn text @click="dialogCancel">Cancel</v-btn>
<v-btn text class="primary--text"
@click="dialogConfirm"
>Save changes
</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 { UserRoles } from '@/types/enum-users'
import Vue from 'vue'
export default Vue.extend({
name: 'UserEditDialog',
data: () => {
return {
UserRoles,
snackbar: false,
snackText: '',
modal: false,
roles: [] as string[],
}
},
props: {
value: Boolean,
user: {
type: Object,
required: true,
},
},
watch: {
value (val) {
this.modal = val
},
modal (val) {
!val && this.dialogCancel()
},
user (val) {
this.dialogReset(val)
},
},
computed: {
libraries (): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
},
},
methods: {
dialogReset (user: UserDto) {
this.roles = user.roles
},
dialogCancel () {
this.$emit('input', false)
this.dialogReset(this.user)
},
dialogConfirm () {
this.editUser()
this.$emit('input', false)
this.dialogReset(this.user)
},
showSnack (message: string) {
this.snackText = message
this.snackbar = true
},
async editUser () {
try {
const roles = {
roles: this.roles,
} as RolesUpdateDto
await this.$store.dispatch('updateUserRoles', { userId: this.user.id, roles: roles })
} catch (e) {
this.showSnack(e.message)
}
},
},
})
</script>
<style scoped>
</style>

View file

@ -1,4 +1,5 @@
import KomgaUsersService from '@/services/komga-users.service'
import { UserRoles } from '@/types/enum-users'
import { AxiosInstance } from 'axios'
import _Vue from 'vue'
import { Module } from 'vuex/types'
@ -11,7 +12,9 @@ const vuexModule: Module<any, any> = {
users: [] as UserWithSharedLibrariesDto[],
},
getters: {
meAdmin: state => state.me.hasOwnProperty('roles') && state.me.roles.includes('ADMIN'),
meAdmin: state => state.me.hasOwnProperty('roles') && state.me.roles.includes(UserRoles.ADMIN),
meFileDownload: state => state.me.hasOwnProperty('roles') && state.me.roles.includes(UserRoles.FILE_DOWNLOAD),
mePageStreaming: state => state.me.hasOwnProperty('roles') && state.me.roles.includes(UserRoles.PAGE_STREAMING),
authenticated: state => state.me.hasOwnProperty('id'),
},
mutations: {
@ -46,6 +49,10 @@ const vuexModule: Module<any, any> = {
await service.postUser(user)
dispatch('getAllUsers')
},
async updateUserRoles ({ dispatch }, { userId, roles }: { userId: number, roles: RolesUpdateDto }) {
await service.patchUserRoles(userId, roles)
dispatch('getAllUsers')
},
async deleteUser ({ dispatch }, user: UserDto) {
await service.deleteUser(user)
dispatch('getAllUsers')

View file

@ -70,6 +70,18 @@ export default class KomgaUsersService {
}
}
async patchUserRoles (userId: number, roles: RolesUpdateDto): Promise<UserDto> {
try {
return (await this.http.patch(`${API_USERS}/${userId}`, roles)).data
} catch (e) {
let msg = `An error occurred while trying to patch user '${userId}'`
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}`)

View file

@ -0,0 +1,5 @@
export enum UserRoles {
ADMIN = 'ADMIN',
FILE_DOWNLOAD = 'FILE_DOWNLOAD',
PAGE_STREAMING = 'PAGE_STREAMING'
}

View file

@ -29,3 +29,7 @@ interface SharedLibrariesUpdateDto {
all: boolean,
libraryIds: number[]
}
interface RolesUpdateDto {
roles: string[]
}

View file

@ -107,6 +107,7 @@
<v-btn icon
title="Download file"
class="pb-1"
:disabled="!canDownload"
:href="fileUrl">
<v-icon>mdi-file-download</v-icon>
</v-btn>
@ -117,7 +118,7 @@
title="Read book"
class="pb-1"
:to="{name: 'read-book', params: { bookId: bookId}}"
:disabled="book.media.status !== 'READY'"
:disabled="book.media.status !== 'READY' || !canReadPages"
>
<v-icon>mdi-book-open-page-variant</v-icon>
</v-btn>
@ -216,6 +217,12 @@ export default Vue.extend({
isAdmin (): boolean {
return this.$store.getters.meAdmin
},
canReadPages (): boolean {
return this.$store.getters.mePageStreaming
},
canDownload (): boolean {
return this.$store.getters.meFileDownload
},
thumbnailUrl (): string {
return bookThumbnailUrl(this.bookId)
},

View file

@ -15,11 +15,11 @@
<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-if="u.roles.includes(UserRoles.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>
<span>{{ u.roles.includes(UserRoles.ADMIN) ? 'Administrator' : 'User' }}</span>
</v-tooltip>
<v-list-item-content>
@ -31,7 +31,8 @@
<v-list-item-action>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn icon @click="editUser(u)" :disabled="u.roles.includes('ADMIN')" v-on="on">
<v-btn icon @click="editSharedLibraries(u)" :disabled="u.roles.includes(UserRoles.ADMIN)"
v-on="on">
<v-icon>mdi-library-books</v-icon>
</v-btn>
</template>
@ -39,6 +40,17 @@
</v-tooltip>
</v-list-item-action>
<v-list-item-action>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn icon @click="editUser(u)" :disabled="u.id === me.id" v-on="on">
<v-icon>mdi-pencil</v-icon>
</v-btn>
</template>
<span>Edit user</span>
</v-tooltip>
</v-list-item-action>
<v-list-item-action>
<v-btn icon @click="promptDeleteUser(u)"
:disabled="u.id === me.id"
@ -59,10 +71,14 @@
<v-icon>mdi-plus</v-icon>
</v-btn>
<user-shared-libraries-edit-dialog v-model="modalUserSharedLibraries"
<user-shared-libraries-edit-dialog v-model="modalEditSharedLibraries"
:user="userToEditSharedLibraries"
/>
<user-edit-dialog v-model="modalEditUser"
:user="userToEdit"
/>
<user-delete-dialog v-model="modalDeleteUser"
:user="userToDelete">
</user-delete-dialog>
@ -76,18 +92,23 @@
<script lang="ts">
import UserDeleteDialog from '@/components/UserDeleteDialog.vue'
import UserEditDialog from '@/components/UserEditDialog.vue'
import UserSharedLibrariesEditDialog from '@/components/UserSharedLibrariesEditDialog.vue'
import { UserRoles } from '@/types/enum-users'
import Vue from 'vue'
export default Vue.extend({
name: 'SettingsUsers',
components: { UserSharedLibrariesEditDialog, UserDeleteDialog },
components: { UserSharedLibrariesEditDialog, UserDeleteDialog, UserEditDialog },
data: () => ({
modalDeleteUser: false,
UserRoles,
modalAddUser: false,
modalDeleteUser: false,
userToDelete: {} as UserDto,
modalUserSharedLibraries: false,
modalEditSharedLibraries: false,
userToEditSharedLibraries: {} as UserWithSharedLibrariesDto,
modalEditUser: false,
userToEdit: {} as UserDto,
}),
computed: {
users (): UserWithSharedLibrariesDto[] {
@ -105,9 +126,13 @@ export default Vue.extend({
this.userToDelete = user
this.modalDeleteUser = true
},
editUser (user: UserWithSharedLibrariesDto) {
editSharedLibraries (user: UserWithSharedLibrariesDto) {
this.userToEditSharedLibraries = user
this.modalUserSharedLibraries = true
this.modalEditSharedLibraries = true
},
editUser (user: UserDto) {
this.userToEdit = user
this.modalEditUser = true
},
},
})