Skip to content

Commit

Permalink
feat(webui): display authentication activity
Browse files Browse the repository at this point in the history
closes #160
  • Loading branch information
gotson committed Jun 25, 2021
1 parent de96e0d commit 9d33602
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 126 deletions.
103 changes: 103 additions & 0 deletions komga-webui/src/components/AuthenticationActivityTable.vue
@@ -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 changes: 138 additions & 0 deletions komga-webui/src/components/UsersList.vue
@@ -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>
15 changes: 14 additions & 1 deletion komga-webui/src/locales/en.json
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
}
}
34 changes: 33 additions & 1 deletion komga-webui/src/services/komga-users.service.ts
@@ -1,4 +1,6 @@
import { AxiosInstance } from 'axios'
import {AxiosInstance} from 'axios'

const qs = require('qs')

const API_USERS = '/api/v1/users'

Expand Down Expand Up @@ -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)
}
}
}
10 changes: 10 additions & 0 deletions komga-webui/src/types/komga-users.ts
Expand Up @@ -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,
}
18 changes: 16 additions & 2 deletions komga-webui/src/views/AccountSettings.vue
@@ -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>
Expand All @@ -11,6 +12,7 @@
/>
</v-col>
</v-row>

<v-row align="center">
<v-col>
<span>{{ $t('common.roles') }}</span>
Expand All @@ -22,6 +24,7 @@
</v-chip-group>
</v-col>
</v-row>

<v-row>
<v-col>
<v-btn color="primary"
Expand All @@ -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"
/>
Expand All @@ -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,
Expand Down

0 comments on commit 9d33602

Please sign in to comment.