feat(webui): edit user restrictions

This commit is contained in:
Gauthier Roebroeck 2022-03-03 08:57:05 +08:00
parent 093610e186
commit 37dfa923e9
13 changed files with 331 additions and 155 deletions

View file

@ -33,6 +33,7 @@
<script lang="ts">
import Vue from 'vue'
import {ERROR} from '@/types/events'
import { AuthenticationActivityDto } from '@/types/komga-users'
export default Vue.extend({
name: 'AuthenticationActivityTable',

View file

@ -40,12 +40,12 @@
<v-list-item-action>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn icon @click="editSharedLibraries(u)" :disabled="u.roles.includes(UserRoles.ADMIN)"
<v-btn icon @click="editRestrictions(u)" :disabled="u.roles.includes(UserRoles.ADMIN)"
v-on="on">
<v-icon>mdi-book-lock</v-icon>
</v-btn>
</template>
<span>{{ $t('settings_user.edit_shared_libraries') }}</span>
<span>{{ $t('settings_user.edit_restrictions') }}</span>
</v-tooltip>
</v-list-item-action>
@ -93,8 +93,8 @@
<v-icon>mdi-plus</v-icon>
</v-btn>
<user-shared-libraries-edit-dialog v-model="modalEditSharedLibraries"
:user="userToEditSharedLibraries"
<user-restrictions-edit-dialog v-model="modalEditRestrictions"
:user="userToEditRestrictions"
/>
<password-change-dialog v-model="modalChangePassword"
@ -121,18 +121,19 @@
<script lang="ts">
import UserEditDialog from '@/components/dialogs/UserEditDialog.vue'
import UserSharedLibrariesEditDialog from '@/components/dialogs/UserSharedLibrariesEditDialog.vue'
import UserRestrictionsEditDialog from '@/components/dialogs/UserRestrictionsEditDialog.vue'
import {UserRoles} from '@/types/enum-users'
import Vue from 'vue'
import PasswordChangeDialog from '@/components/dialogs/PasswordChangeDialog.vue'
import {ERROR} from '@/types/events'
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
import { UserDto } from '@/types/komga-users'
export default Vue.extend({
name: 'UsersList',
components: {
ConfirmationDialog,
UserSharedLibrariesEditDialog,
UserRestrictionsEditDialog,
UserEditDialog,
PasswordChangeDialog,
},
@ -141,8 +142,8 @@ export default Vue.extend({
modalAddUser: false,
modalDeleteUser: false,
userToDelete: {} as UserDto,
modalEditSharedLibraries: false,
userToEditSharedLibraries: {} as UserDto,
modalEditRestrictions: false,
userToEditRestrictions: {} as UserDto,
modalEditUser: false,
userToEdit: {} as UserDto,
modalChangePassword: false,
@ -175,9 +176,9 @@ export default Vue.extend({
this.userToDelete = user
this.modalDeleteUser = true
},
editSharedLibraries(user: UserDto) {
this.userToEditSharedLibraries = user
this.modalEditSharedLibraries = true
editRestrictions(user: UserDto) {
this.userToEditRestrictions = user
this.modalEditRestrictions = true
},
editUser(user: UserDto) {
this.userToEdit = user

View file

@ -59,6 +59,7 @@
import Vue from 'vue'
import {required, sameAs} from 'vuelidate/lib/validators'
import {ERROR} from '@/types/events'
import { PasswordUpdateDto } from '@/types/komga-users'
export default Vue.extend({
name: 'PasswordChangeDialog',

View file

@ -56,6 +56,7 @@ import {UserRoles} from '@/types/enum-users'
import Vue from 'vue'
import {ERROR} from '@/types/events'
import {LibraryDto} from '@/types/komga-libraries'
import {UserDto, UserUpdateDto} from '@/types/komga-users'
export default Vue.extend({
name: 'UserEditDialog',

View file

@ -0,0 +1,264 @@
<template>
<v-dialog v-model="modal"
:fullscreen="$vuetify.breakpoint.xsOnly"
max-width="600"
>
<v-card>
<v-form v-model="form" ref="form">
<v-toolbar class="hidden-sm-and-up">
<v-btn icon @click="dialogCancel">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>
{{ $t('dialog.edit_user_restrictions.edit_restrictions_for', {name: user.email}) }}
</v-toolbar-title>
<v-spacer/>
<v-toolbar-items>
<v-btn text
color="primary"
:disabled="!form"
@click="dialogConfirm">{{ $t('common.save_changes') }}</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-card-title class="hidden-xs-only">
{{ $t('dialog.edit_user_restrictions.edit_restrictions_for', {name: user.email}) }}
</v-card-title>
<v-tabs v-model="tab" grow>
<v-tab>{{ $t('dialog.edit_user_restrictions.tab_shared_libraries') }}</v-tab>
<v-tab>{{ $t('dialog.edit_user_restrictions.tab_content_restrictions') }}</v-tab>
<!-- Tab: Shared Libraries -->
<v-tab-item>
<v-card flat>
<v-container fluid class="pa-6">
<v-row>
<v-col>
<v-checkbox v-model="allLibraries"
:label="$t('common.all_libraries')"
hide-details
class="my-0 py-0"
/>
</v-col>
</v-row>
<v-divider class="my-2"/>
<v-row v-for="(l, index) in libraries" :key="index">
<v-col>
<v-checkbox v-model="selectedLibraries"
:label="l.name"
:value="l.id"
:disabled="allLibraries"
hide-details
class="my-0 py-0"
/>
</v-col>
</v-row>
</v-container>
</v-card>
</v-tab-item>
<!-- Tab: Content Restrictions -->
<v-tab-item>
<v-card flat>
<v-container fluid class="pa-6">
<v-row align="center">
<v-col>
<v-select v-model="ageRestriction"
:label="$t('dialog.edit_user_restrictions.label_age_restriction')"
:items="ageRestrictionsAvailable"
filled
>
</v-select>
</v-col>
<v-col>
<v-text-field v-model="age"
:disabled="ageRestriction === NONE"
:label="$t('common.age')"
:rules="[ageRules]"
filled
dense
type="number"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<span class="text-body-2">{{ $t('dialog.edit_user_restrictions.label_allow_only_labels') }}</span>
<v-combobox v-model="labelsAllow"
:items="sharingLabelsAvailable"
hide-selected
chips
deletable-chips
multiple
filled
dense
>
</v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<span class="text-body-2">{{ $t('dialog.edit_user_restrictions.label_exclude_labels') }}</span>
<v-combobox v-model="labelsExclude"
:items="sharingLabelsAvailable"
hide-selected
chips
deletable-chips
multiple
filled
dense
>
</v-combobox>
</v-col>
</v-row>
</v-container>
</v-card>
</v-tab-item>
</v-tabs>
<v-card-actions class="hidden-xs-only">
<v-spacer/>
<v-btn text @click="dialogCancel">{{ $t('common.cancel') }}</v-btn>
<v-btn color="primary"
@click="dialogConfirm"
:disabled="!form"
>{{ $t('common.save_changes') }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import Vue from 'vue'
import {ERROR} from '@/types/events'
import {LibraryDto} from '@/types/komga-libraries'
import {UserDto, UserUpdateDto} from '@/types/komga-users'
import {AllowExclude} from '@/types/enum-users'
const NONE = 'NONE'
export default Vue.extend({
name: 'UserRestrictionsEditDialog',
data: () => {
return {
NONE,
tab: 0,
form: false,
modal: false,
allLibraries: true,
selectedLibraries: [] as string[],
labelsAllow: [] as string[],
labelsExclude: [] as string[],
sharingLabelsAvailable: [] as string[],
ageRestriction: NONE as AllowExclude | string,
age: 0,
}
},
props: {
value: Boolean,
user: {
type: Object,
required: true,
},
},
watch: {
value(val) {
this.modal = val
},
modal(val) {
if (val) {
this.loadAvailableSharingLabels()
} else {
this.dialogCancel()
}
},
user(val) {
this.dialogReset(val)
this.loadAvailableSharingLabels()
},
},
computed: {
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
},
ageRestrictionsAvailable(): any[] {
return [
{text: this.$t('dialog.edit_user_restrictions.age_restriction.none').toString(), value: NONE},
{text: this.$t('dialog.edit_user_restrictions.age_restriction.allow_under').toString(), value: AllowExclude.ALLOW_ONLY},
{text: this.$t('dialog.edit_user_restrictions.age_restriction.exclude_over').toString(), value: AllowExclude.EXCLUDE},
]
},
},
methods: {
async loadAvailableSharingLabels() {
this.sharingLabelsAvailable = await this.$komgaReferential.getSharingLabels()
},
ageRules(age: number): boolean | string {
if (age < 0) return this.$t('validation.zero_or_more').toString()
return true
},
dialogReset(user: UserDto) {
(this.$refs.form as any)?.resetValidation()
this.tab = 0
this.allLibraries = user.sharedAllLibraries
if (user.sharedAllLibraries) {
this.selectedLibraries = this.libraries.map(x => x.id)
} else {
this.selectedLibraries = user.sharedLibrariesIds
}
this.labelsAllow = user.labelsAllow
this.labelsExclude = user.labelsExclude
this.ageRestriction = user.ageRestriction?.restriction || NONE
this.age = user.ageRestriction?.age || 0
},
dialogCancel() {
this.$emit('input', false)
this.dialogReset(this.user)
},
dialogConfirm() {
this.editUser()
this.$emit('input', false)
this.dialogReset(this.user)
},
async editUser() {
try {
if(!(this.$refs.form as any).validate()) return
const patch = {
sharedLibraries: {
all: this.allLibraries,
libraryIds: this.selectedLibraries,
},
labelsAllow: this.labelsAllow,
labelsExclude: this.labelsExclude,
} as UserUpdateDto
if (this.ageRestriction !== NONE) {
patch.ageRestriction = {
age: this.age,
restriction: this.ageRestriction as AllowExclude,
}
} else {
this.$_.merge(patch, {ageRestriction: null})
}
await this.$store.dispatch('updateUser', {userId: this.user.id, patch: patch})
} catch (e) {
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
}
},
},
})
</script>
<style scoped>
</style>

View file

@ -1,131 +0,0 @@
<template>
<v-dialog v-model="modal"
max-width="450"
>
<v-card>
<v-card-title>{{ $t('dialog.edit_user_shared_libraries.dialog_title') }}</v-card-title>
<v-card-text>
<v-container fluid>
<v-row>
<v-col>
<span class="text-subtitle-1">{{
$t('dialog.edit_user_shared_libraries.label_shared_with', {name: user.email})
}}</span>
</v-col>
</v-row>
<v-row>
<v-col>
<v-checkbox v-model="allLibraries"
:label="$t('dialog.edit_user_shared_libraries.field_all_libraries')"
hide-details
class="my-0 py-0"
/>
</v-col>
</v-row>
<v-divider class="my-2"/>
<v-row v-for="(l, index) in libraries" :key="index">
<v-col>
<v-checkbox v-model="selectedLibraries"
:label="l.name"
:value="l.id"
:disabled="allLibraries"
hide-details
class="my-0 py-0"
/>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn text @click="dialogCancel">{{ $t('dialog.edit_user_shared_libraries.button_cancel') }}</v-btn>
<v-btn color="primary"
@click="dialogConfirm"
>{{ $t('dialog.edit_user_shared_libraries.button_confirm') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import Vue from 'vue'
import {ERROR} from '@/types/events'
import {LibraryDto} from '@/types/komga-libraries'
export default Vue.extend({
name: 'UserSharedLibrariesEditDialog',
data: () => {
return {
modal: false,
allLibraries: true,
selectedLibraries: [] 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.allLibraries = user.sharedAllLibraries
if (user.sharedAllLibraries) {
this.selectedLibraries = this.libraries.map(x => x.id)
} else {
this.selectedLibraries = user.sharedLibrariesIds
}
},
dialogCancel() {
this.$emit('input', false)
this.dialogReset(this.user)
},
dialogConfirm() {
this.editUser()
this.$emit('input', false)
this.dialogReset(this.user)
},
async editUser() {
try {
const patch = {
sharedLibraries: {
all: this.allLibraries,
libraryIds: this.selectedLibraries,
},
} as UserUpdateDto
await this.$store.dispatch('updateUser', {userId: this.user.id, patch: patch})
} catch (e) {
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
}
},
},
})
</script>
<style scoped>
</style>

View file

@ -180,6 +180,7 @@
"title": "{name} collection"
},
"common": {
"age": "Age",
"all_libraries": "All Libraries",
"books": "Books",
"books_n": "No book | 1 book | {count} books",
@ -218,6 +219,7 @@
"required": "Required",
"reset_filters": "Reset filters",
"roles": "Roles",
"save_changes": "Save changes",
"series": "Series",
"sidecars": "Sidecars",
"tags": "Tags",
@ -453,12 +455,18 @@
"dialog_title": "Edit user",
"label_roles_for": "Roles for {name}"
},
"edit_user_shared_libraries": {
"button_cancel": "Cancel",
"button_confirm": "Save changes",
"dialog_title": "Edit shared libraries",
"field_all_libraries": "All libraries",
"label_shared_with": "Shared with {name}"
"edit_user_restrictions": {
"age_restriction": {
"allow_under": "Allow only under",
"exclude_over": "Exclude over",
"none": "No restriction"
},
"edit_restrictions_for": "Edit restrictions for {name}",
"label_age_restriction": "Age restriction",
"label_allow_only_labels": "Allow only labels",
"label_exclude_labels": "Exclude labels",
"tab_content_restrictions": "Content Restrictions",
"tab_shared_libraries": "Shared Libraries"
},
"empty_trash": {
"body": "By default the media server doesn't remove information for media right away. This helps if a drive is temporarily disconnected. When you empty the trash for a library, all information about missing media is deleted.",
@ -760,7 +768,7 @@
},
"settings_user": {
"change_password": "Change password",
"edit_shared_libraries": "Edit shared libraries",
"edit_restrictions": "Edit restrictions",
"edit_user": "Edit user",
"latest_activity": "Latest activity: {date}",
"no_recent_activity": "No recent activity",
@ -804,6 +812,9 @@
"authentication_activity": "Authentication Activity",
"users": "Users"
},
"validation": {
"zero_or_more": "Must be 0 or more"
},
"welcome": {
"add_library": "Add library",
"no_libraries_yet": "No libraries have been added yet!",

View file

@ -3,6 +3,7 @@ import {UserRoles} from '@/types/enum-users'
import {AxiosInstance} from 'axios'
import _Vue from 'vue'
import {Module} from 'vuex/types'
import {UserCreationDto, UserDto, UserUpdateDto} from '@/types/komga-users'
let service: KomgaUsersService

View file

@ -1,4 +1,5 @@
import { AxiosInstance } from 'axios'
import {UserDto} from '@/types/komga-users'
const API_CLAIM = '/api/v1/claim'

View file

@ -1,4 +1,11 @@
import {AxiosInstance} from 'axios'
import {
AuthenticationActivityDto,
PasswordUpdateDto,
UserCreationDto,
UserDto,
UserUpdateDto,
} from '@/types/komga-users'
const qs = require('qs')

View file

@ -3,3 +3,7 @@ export enum UserRoles {
FILE_DOWNLOAD = 'FILE_DOWNLOAD',
PAGE_STREAMING = 'PAGE_STREAMING'
}
export enum AllowExclude {
ALLOW_ONLY = 'ALLOW_ONLY', EXCLUDE = 'EXCLUDE'
}

View file

@ -1,29 +1,43 @@
interface UserDto {
import {AllowExclude} from '@/types/enum-users'
export interface UserDto {
id: string,
email: string,
roles: string[],
sharedAllLibraries: boolean,
sharedLibrariesIds: string[]
sharedLibrariesIds: string[],
labelsAllow: string[],
labelsExclude: string[],
ageRestriction?: {
age: number,
restriction: AllowExclude,
},
}
interface UserCreationDto {
export interface UserCreationDto {
email: string,
roles: string[]
}
interface PasswordUpdateDto {
export interface PasswordUpdateDto {
password: string
}
interface UserUpdateDto {
export interface UserUpdateDto {
roles?: string[],
sharedLibraries?: {
all: boolean,
libraryIds: string[]
},
ageRestriction?: {
age: number,
restriction: AllowExclude,
}
labelsAllow?: string[],
labelsExclude?: string[],
}
interface AuthenticationActivityDto {
export interface AuthenticationActivityDto {
userId?: string,
email?: string,
ip?: string,

View file

@ -54,6 +54,7 @@
import PasswordChangeDialog from '@/components/dialogs/PasswordChangeDialog.vue'
import Vue from 'vue'
import AuthenticationActivityTable from '@/components/AuthenticationActivityTable.vue'
import { UserDto } from '@/types/komga-users'
export default Vue.extend({
name: 'AccountSettings',