2024-04-15 00:44:18 +02:00
|
|
|
<script setup>
|
|
|
|
import SearchBox from '@/components/SearchBox.vue'
|
|
|
|
import userService from '@/services/userService'
|
2024-04-15 18:39:37 +02:00
|
|
|
import Spinner from '@/components/Spinner.vue'
|
|
|
|
import { useNotifyStore } from '@/stores/notify'
|
2024-04-15 00:44:18 +02:00
|
|
|
import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
|
|
|
|
|
|
|
const notify = useNotifyStore()
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
userId: [Number, String],
|
|
|
|
lastOnly: Boolean,
|
|
|
|
showSearch: Boolean
|
|
|
|
})
|
|
|
|
|
2024-04-15 18:39:37 +02:00
|
|
|
const periods = {
|
|
|
|
aMonth: 1,
|
|
|
|
threeMonths: 3,
|
|
|
|
halfYear: 6,
|
|
|
|
aYear: 12
|
|
|
|
}
|
|
|
|
|
2024-04-15 00:44:18 +02:00
|
|
|
const authentications = ref([])
|
|
|
|
const isFetching = ref(false)
|
|
|
|
const searched = ref('')
|
2024-04-15 18:39:37 +02:00
|
|
|
const period = ref(periods.aMonth)
|
|
|
|
const orderIsDesc = ref(true)
|
2024-04-15 00:44:18 +02:00
|
|
|
|
|
|
|
const visibleAuthentications = computed(() => {
|
|
|
|
return authentications.value.filter(authentication => {
|
|
|
|
return JSON.stringify(authentication)
|
|
|
|
.toString()
|
|
|
|
.toLowerCase()
|
|
|
|
.includes(searched.value);
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
getAuthentications()
|
|
|
|
})
|
|
|
|
|
2024-04-15 18:39:37 +02:00
|
|
|
/**
|
|
|
|
* Sets the visible time span
|
|
|
|
*
|
|
|
|
* @param {int} duration
|
|
|
|
*/
|
|
|
|
function setPeriod(duration) {
|
|
|
|
period.value = duration
|
|
|
|
getAuthentications()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set sort order to ASC
|
|
|
|
*/
|
|
|
|
function setAsc() {
|
|
|
|
orderIsDesc.value = false
|
|
|
|
sortAsc()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sorts entries ascending
|
|
|
|
*/
|
|
|
|
function sortAsc() {
|
|
|
|
authentications.value.sort((a, b) => a.id > b.id ? 1 : -1)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set sort order to DESC
|
|
|
|
*/
|
|
|
|
function setDesc() {
|
|
|
|
orderIsDesc.value = true
|
|
|
|
sortDesc()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sorts entries descending
|
|
|
|
*/
|
|
|
|
function sortDesc() {
|
|
|
|
authentications.value.sort((a, b) => a.id < b.id ? 1 : -1)
|
|
|
|
}
|
|
|
|
|
2024-04-15 00:44:18 +02:00
|
|
|
/**
|
|
|
|
* Gets user authentication logs
|
|
|
|
*/
|
|
|
|
function getAuthentications() {
|
|
|
|
isFetching.value = true
|
|
|
|
let limit = props.lastOnly ? 3 : false
|
|
|
|
|
2024-04-15 18:39:37 +02:00
|
|
|
userService.getauthentications(props.userId, period.value, limit, {returnError: true})
|
2024-04-15 00:44:18 +02:00
|
|
|
.then(response => {
|
|
|
|
authentications.value = response.data
|
2024-04-15 18:39:37 +02:00
|
|
|
orderIsDesc.value == true ? sortDesc() : sortAsc()
|
2024-04-15 00:44:18 +02:00
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
notify.error(error)
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
isFetching.value = false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const deviceIcon = (device) => {
|
|
|
|
switch (device) {
|
|
|
|
case "phone":
|
|
|
|
return 'mobile-screen'
|
|
|
|
case "tablet":
|
|
|
|
return 'tablet-screen-button'
|
|
|
|
default:
|
|
|
|
return 'display'
|
|
|
|
}
|
|
|
|
}
|
2024-04-15 18:34:29 +02:00
|
|
|
|
|
|
|
const isSuccessfulLogin = (authentication) => {
|
|
|
|
return authentication.login_successful && authentication.login_at
|
|
|
|
}
|
|
|
|
|
|
|
|
const isSuccessfulLogout = (authentication) => {
|
|
|
|
return !authentication.login_at && authentication.logout_at
|
|
|
|
}
|
|
|
|
|
|
|
|
const isFailedEntry = (authentication) => {
|
|
|
|
return !authentication.login_successful && !authentication.logout_at
|
|
|
|
}
|
2024-04-15 00:44:18 +02:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<SearchBox v-if="props.showSearch" v-model:keyword="searched" :hasNoBackground="true" />
|
2024-04-15 18:39:37 +02:00
|
|
|
<nav v-if="props.showSearch" class="level is-mobile">
|
|
|
|
<div class="level-item has-text-centered">
|
|
|
|
<div class="buttons">
|
|
|
|
<button id="btnShowOneMonth" :title="$t('admin.show_last_month_log')" v-on:click="setPeriod(periods.aMonth)" :class="{ 'has-text-grey' : period !== periods.aMonth }" class="button is-ghost p-1">1m</button>
|
|
|
|
<button id="btnShowThreeMonths" :title="$t('admin.show_three_months_log')" v-on:click="setPeriod(periods.threeMonths)" :class="{ 'has-text-grey' : period !== periods.threeMonths }" class="button is-ghost p-1">3m</button>
|
|
|
|
<button id="btnShowSixMonths" :title="$t('admin.show_six_months_log')" v-on:click="setPeriod(periods.halfYear)" :class="{ 'has-text-grey' : period !== periods.halfYear }" class="button is-ghost p-1">6m</button>
|
|
|
|
<button id="btnShowOneYear" :title="$t('admin.show_one_year_log')" v-on:click="setPeriod(periods.aYear)" :class="{ 'has-text-grey' : period !== periods.aYear }" class="button is-ghost p-1 mr-5">1y</button>
|
|
|
|
<button id="btnSortLogDesc" v-on:click="setDesc" :title="$t('admin.sort_by_date_desc')" :class="{ 'has-text-grey' : !orderIsDesc }" class="button p-1 is-ghost">
|
|
|
|
<FontAwesomeIcon :icon="['fas', 'arrow-up-long']" flip="vertical" />
|
|
|
|
<FontAwesomeIcon :icon="['far', 'calendar']" />
|
|
|
|
</button>
|
|
|
|
<button id="btnSortLogAsc" v-on:click="setAsc" :title="$t('admin.sort_by_date_asc')" :class="{ 'has-text-grey' : orderIsDesc }" class="button p-1 is-ghost">
|
|
|
|
<FontAwesomeIcon :icon="['fas', 'arrow-up-long']" />
|
|
|
|
<FontAwesomeIcon :icon="['far', 'calendar']" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</nav>
|
2024-04-15 00:44:18 +02:00
|
|
|
<div v-if="visibleAuthentications.length > 0">
|
|
|
|
<div v-for="authentication in visibleAuthentications" :key="authentication.id" class="list-item is-size-6 is-size-7-mobile has-text-grey is-flex is-justify-content-space-between">
|
|
|
|
<div>
|
2024-04-15 18:34:29 +02:00
|
|
|
<div>
|
|
|
|
<span v-if="isFailedEntry(authentication)" v-html="$t('admin.failed_login_on', { login_at: authentication.login_at })" />
|
|
|
|
<span v-else-if="isSuccessfulLogout(authentication)" v-html="$t('admin.successful_logout_on', { login_at: authentication.logout_at })" />
|
|
|
|
<span v-else v-html="$t('admin.successful_login_on', { login_at: authentication.login_at })" />
|
2024-04-15 00:44:18 +02:00
|
|
|
</div>
|
|
|
|
<div>
|
2024-04-15 18:34:29 +02:00
|
|
|
{{ $t('commons.IP') }}: <span class="has-text-grey-light">{{ authentication.ip_address }}</span> -
|
|
|
|
{{ $t('commons.browser') }}: <span class="has-text-grey-light">{{ authentication.browser }}</span> -
|
|
|
|
{{ $t('commons.operating_system_short') }}: <span class="has-text-grey-light">{{ authentication.platform }}</span>
|
2024-04-15 00:44:18 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="is-align-self-center has-text-grey-darker">
|
|
|
|
<font-awesome-layers class="fa-2x">
|
|
|
|
<FontAwesomeIcon :icon="['fas', deviceIcon(authentication.device)]" transform="grow-6" fixed-width />
|
2024-04-15 18:34:29 +02:00
|
|
|
<FontAwesomeIcon :icon="['fas', isFailedEntry(authentication) ? 'times' : 'check']"
|
|
|
|
:transform="'shrink-7' + (authentication.device == 'desktop' ? ' up-2' : '')"
|
|
|
|
:class="isFailedEntry(authentication) ? 'has-text-danger-dark' : 'has-text-success-dark'" fixed-width />
|
2024-04-15 00:44:18 +02:00
|
|
|
</font-awesome-layers>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div v-else-if="authentications.length == 0" class="mt-4">
|
|
|
|
{{ $t('commons.no_entry_yet') }}
|
|
|
|
</div>
|
|
|
|
<div v-else class="mt-5 pl-3">
|
|
|
|
{{ $t('commons.no_result') }}
|
|
|
|
</div>
|
2024-04-15 18:39:37 +02:00
|
|
|
<Spinner :isVisible="isFetching" />
|
2024-04-15 00:44:18 +02:00
|
|
|
</template>
|