mirror of
https://github.com/gotson/komga.git
synced 2025-12-20 15:34:17 +01:00
parent
de96e0dcef
commit
9d33602873
7 changed files with 327 additions and 126 deletions
103
komga-webui/src/components/AuthenticationActivityTable.vue
Normal file
103
komga-webui/src/components/AuthenticationActivityTable.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:options.sync="options"
|
||||
:server-items-length="totalItems"
|
||||
:loading="loading"
|
||||
sort-by="dateTime"
|
||||
:sort-desc="true"
|
||||
multi-sort
|
||||
class="elevation-1"
|
||||
:footer-props="{
|
||||
itemsPerPageOptions: [20, 50, 100]
|
||||
}"
|
||||
>
|
||||
<template v-slot:item.success="{ item }">
|
||||
<v-icon v-if="item.success" color="success">mdi-check-circle</v-icon>
|
||||
<v-icon v-else color="error">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.dateTime="{ item }">
|
||||
{{
|
||||
new Intl.DateTimeFormat($i18n.locale, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short'
|
||||
}).format(new Date(item.dateTime))
|
||||
}}
|
||||
</template>
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'AuthenticationActivityTable',
|
||||
data: function () {
|
||||
return {
|
||||
items: [] as AuthenticationActivityDto[],
|
||||
totalItems: 0,
|
||||
loading: true,
|
||||
options: {} as any,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
forMe: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
options: {
|
||||
handler() {
|
||||
this.loadData()
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
headers(): object[] {
|
||||
const headers = []
|
||||
if (!this.forMe) headers.push({text: this.$t('authentication_activity.email').toString(), value: 'email'})
|
||||
headers.push(
|
||||
{text: this.$t('authentication_activity.ip').toString(), value: 'ip'},
|
||||
{text: this.$t('authentication_activity.user_agent').toString(), value: 'userAgent'},
|
||||
{text: this.$t('authentication_activity.success').toString(), value: 'success'},
|
||||
{text: this.$t('authentication_activity.error').toString(), value: 'error'},
|
||||
{text: this.$t('authentication_activity.datetime').toString(), value: 'dateTime'},
|
||||
)
|
||||
return headers
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
|
||||
const {sortBy, sortDesc, page, itemsPerPage} = this.options
|
||||
|
||||
const pageRequest = {
|
||||
page: page - 1,
|
||||
size: itemsPerPage,
|
||||
sort: [],
|
||||
} as PageRequest
|
||||
|
||||
for (let i = 0; i < sortBy.length; i++) {
|
||||
pageRequest.sort!!.push(`${sortBy[i]},${sortDesc[i] ? 'desc' : 'asc'}`)
|
||||
}
|
||||
|
||||
let itemsPage
|
||||
if (!this.forMe) itemsPage = await this.$komgaUsers.getAuthenticationActivity(pageRequest)
|
||||
else itemsPage = await this.$komgaUsers.getMyAuthenticationActivity(pageRequest)
|
||||
|
||||
this.totalItems = itemsPage.totalElements
|
||||
this.items = itemsPage.content
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
138
komga-webui/src/components/UsersList.vue
Normal file
138
komga-webui/src/components/UsersList.vue
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<template>
|
||||
<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(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(UserRoles.ADMIN) ? $t('settings_user.role_administrator') : $t('settings_user.role_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-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn icon @click="editSharedLibraries(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>
|
||||
</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>{{ $t('settings_user.edit_user') }}</span>
|
||||
</v-tooltip>
|
||||
</v-list-item-action>
|
||||
|
||||
<v-list-item-action>
|
||||
<v-btn icon @click="promptDeleteUser(u)"
|
||||
:disabled="u.id === me.id"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="index !== users.length-1"/>
|
||||
</div>
|
||||
</v-list>
|
||||
|
||||
<v-btn fab absolute bottom color="primary"
|
||||
:right="!$vuetify.rtl"
|
||||
:left="$vuetify.rtl"
|
||||
class="mx-6"
|
||||
small
|
||||
:to="{name: 'settings-users-add'}">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<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>
|
||||
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import UserDeleteDialog from '@/components/dialogs/UserDeleteDialog.vue'
|
||||
import UserEditDialog from '@/components/dialogs/UserEditDialog.vue'
|
||||
import UserSharedLibrariesEditDialog from '@/components/dialogs/UserSharedLibrariesEditDialog.vue'
|
||||
import {UserRoles} from '@/types/enum-users'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'UsersList',
|
||||
components: {UserSharedLibrariesEditDialog, UserDeleteDialog, UserEditDialog},
|
||||
data: () => ({
|
||||
UserRoles,
|
||||
modalAddUser: false,
|
||||
modalDeleteUser: false,
|
||||
userToDelete: {} as UserDto,
|
||||
modalEditSharedLibraries: false,
|
||||
userToEditSharedLibraries: {} as UserWithSharedLibrariesDto,
|
||||
modalEditUser: false,
|
||||
userToEdit: {} as UserDto,
|
||||
}),
|
||||
computed: {
|
||||
users(): UserWithSharedLibrariesDto[] {
|
||||
return this.$store.state.komgaUsers.users
|
||||
},
|
||||
me(): UserDto {
|
||||
return this.$store.state.komgaUsers.me
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.$store.dispatch('getAllUsers')
|
||||
},
|
||||
methods: {
|
||||
promptDeleteUser(user: UserDto) {
|
||||
this.userToDelete = user
|
||||
this.modalDeleteUser = true
|
||||
},
|
||||
editSharedLibraries(user: UserWithSharedLibrariesDto) {
|
||||
this.userToEditSharedLibraries = user
|
||||
this.modalEditSharedLibraries = true
|
||||
},
|
||||
editUser(user: UserDto) {
|
||||
this.userToEdit = user
|
||||
this.modalEditUser = true
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -3,6 +3,10 @@
|
|||
"dataFooter": {
|
||||
"pageText": "{0}-{1} of {2}"
|
||||
},
|
||||
"dataIterator": {
|
||||
"noResultsText": "No matching records found",
|
||||
"loadingText": "Loading items..."
|
||||
},
|
||||
"dataTable": {
|
||||
"itemsPerPageText": "Rows per page:",
|
||||
"sortBy": "Sort by"
|
||||
|
|
@ -604,11 +608,20 @@
|
|||
"USER": "User"
|
||||
},
|
||||
"users": {
|
||||
"users": "Users"
|
||||
"users": "Users",
|
||||
"authentication_activity": "Authentication Activity"
|
||||
},
|
||||
"welcome": {
|
||||
"add_library": "Add library",
|
||||
"no_libraries_yet": "No libraries have been added yet!",
|
||||
"welcome_message": "Welcome to Komga"
|
||||
},
|
||||
"authentication_activity": {
|
||||
"ip": "Ip",
|
||||
"user_agent": "User Agent",
|
||||
"email": "Email",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"datetime": "Date Time"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { AxiosInstance } from 'axios'
|
||||
import {AxiosInstance} from 'axios'
|
||||
|
||||
const qs = require('qs')
|
||||
|
||||
const API_USERS = '/api/v1/users'
|
||||
|
||||
|
|
@ -129,4 +131,34 @@ export default class KomgaUsersService {
|
|||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getMyAuthenticationActivity (pageRequest?: PageRequest): Promise<Page<AuthenticationActivityDto>> {
|
||||
try {
|
||||
return (await this.http.get(`${API_USERS}/me/authentication-activity`, {
|
||||
params: pageRequest,
|
||||
paramsSerializer: params => qs.stringify(params, { indices: false }),
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve authentication activity'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getAuthenticationActivity (pageRequest?: PageRequest): Promise<Page<AuthenticationActivityDto>> {
|
||||
try {
|
||||
return (await this.http.get(`${API_USERS}/authentication-activity`, {
|
||||
params: pageRequest,
|
||||
paramsSerializer: params => qs.stringify(params, { indices: false }),
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve authentication activity'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,3 +33,13 @@ interface SharedLibrariesUpdateDto {
|
|||
interface RolesUpdateDto {
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
interface AuthenticationActivityDto {
|
||||
userId?: string,
|
||||
email?: string,
|
||||
ip?: string,
|
||||
userAgent?: string,
|
||||
success: Boolean,
|
||||
error?: string,
|
||||
dateTime: string,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<template>
|
||||
<v-container fluid class="pa-6">
|
||||
<v-row>
|
||||
<span class="text-h5">{{ $t('account_settings.account_settings') }}</span>
|
||||
<v-col class="text-h5">{{ $t('account_settings.account_settings') }}</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col cols="12" md="8" lg="6" xl="4">
|
||||
<span class="text-capitalize">{{ $t('common.email') }}</span>
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col>
|
||||
<span>{{ $t('common.roles') }}</span>
|
||||
|
|
@ -22,6 +24,7 @@
|
|||
</v-chip-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn color="primary"
|
||||
|
|
@ -30,6 +33,16 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col class="text-h5">{{ $t('users.authentication_activity') }}</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<authentication-activity-table for-me/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<password-change-dialog v-model="modalPasswordChange"
|
||||
:user="me"
|
||||
/>
|
||||
|
|
@ -40,10 +53,11 @@
|
|||
<script lang="ts">
|
||||
import PasswordChangeDialog from '@/components/dialogs/PasswordChangeDialog.vue'
|
||||
import Vue from 'vue'
|
||||
import AuthenticationActivityTable from "@/components/AuthenticationActivityTable.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'AccountSettings',
|
||||
components: { PasswordChangeDialog },
|
||||
components: {AuthenticationActivityTable, PasswordChangeDialog },
|
||||
data: () => {
|
||||
return {
|
||||
modalPasswordChange: false,
|
||||
|
|
|
|||
|
|
@ -1,142 +1,33 @@
|
|||
<template>
|
||||
<v-container fluid class="pa-6">
|
||||
<v-row>
|
||||
<span class="text-h5">{{ $t('users.users') }}</span>
|
||||
<v-col class="text-h5">{{ $t('users.users') }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="8" lg="6" 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(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(UserRoles.ADMIN) ? $t('settings_user.role_administrator') : $t('settings_user.role_user') }}</span>
|
||||
</v-tooltip>
|
||||
<users-list/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ u.email }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
|
||||
<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-on="on">
|
||||
<v-icon>mdi-book-lock</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t('settings_user.edit_shared_libraries') }}</span>
|
||||
</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>{{ $t('settings_user.edit_user') }}</span>
|
||||
</v-tooltip>
|
||||
</v-list-item-action>
|
||||
|
||||
<v-list-item-action>
|
||||
<v-btn icon @click="promptDeleteUser(u)"
|
||||
:disabled="u.id === me.id"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="index !== users.length-1"/>
|
||||
</div>
|
||||
</v-list>
|
||||
|
||||
<v-btn fab absolute bottom color="primary"
|
||||
:right="!$vuetify.rtl"
|
||||
:left="$vuetify.rtl"
|
||||
class="mx-6"
|
||||
small
|
||||
:to="{name: 'settings-users-add'}">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<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>
|
||||
|
||||
<router-view/>
|
||||
</div>
|
||||
<v-row>
|
||||
<v-col class="text-h5">{{ $t('users.authentication_activity') }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<authentication-activity-table/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import UserDeleteDialog from '@/components/dialogs/UserDeleteDialog.vue'
|
||||
import UserEditDialog from '@/components/dialogs/UserEditDialog.vue'
|
||||
import UserSharedLibrariesEditDialog from '@/components/dialogs/UserSharedLibrariesEditDialog.vue'
|
||||
import {UserRoles} from '@/types/enum-users'
|
||||
import Vue from 'vue'
|
||||
import AuthenticationActivityTable from "@/components/AuthenticationActivityTable.vue";
|
||||
import UsersList from "@/components/UsersList.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SettingsUsers',
|
||||
components: { UserSharedLibrariesEditDialog, UserDeleteDialog, UserEditDialog },
|
||||
data: () => ({
|
||||
UserRoles,
|
||||
modalAddUser: false,
|
||||
modalDeleteUser: false,
|
||||
userToDelete: {} as UserDto,
|
||||
modalEditSharedLibraries: false,
|
||||
userToEditSharedLibraries: {} as UserWithSharedLibrariesDto,
|
||||
modalEditUser: false,
|
||||
userToEdit: {} as UserDto,
|
||||
}),
|
||||
computed: {
|
||||
users (): UserWithSharedLibrariesDto[] {
|
||||
return this.$store.state.komgaUsers.users
|
||||
},
|
||||
me (): UserDto {
|
||||
return this.$store.state.komgaUsers.me
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
await this.$store.dispatch('getAllUsers')
|
||||
},
|
||||
methods: {
|
||||
promptDeleteUser (user: UserDto) {
|
||||
this.userToDelete = user
|
||||
this.modalDeleteUser = true
|
||||
},
|
||||
editSharedLibraries (user: UserWithSharedLibrariesDto) {
|
||||
this.userToEditSharedLibraries = user
|
||||
this.modalEditSharedLibraries = true
|
||||
},
|
||||
editUser (user: UserDto) {
|
||||
this.userToEdit = user
|
||||
this.modalEditUser = true
|
||||
},
|
||||
},
|
||||
components: {UsersList, AuthenticationActivityTable },
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue