AccessLogViewer.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <script setup>
  2. import SearchBox from '@/components/SearchBox.vue'
  3. import userService from '@/services/userService'
  4. import Spinner from '@/components/Spinner.vue'
  5. import { useNotifyStore } from '@/stores/notify'
  6. import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
  7. import { UseColorMode } from '@vueuse/components'
  8. const notify = useNotifyStore()
  9. const props = defineProps({
  10. userId: [Number, String],
  11. lastOnly: Boolean,
  12. showSearch: Boolean,
  13. period: {
  14. type: [Number, String],
  15. default: 12
  16. },
  17. })
  18. const periods = {
  19. aMonth: 1,
  20. threeMonths: 3,
  21. halfYear: 6,
  22. aYear: 12
  23. }
  24. const authentications = ref([])
  25. const isFetching = ref(false)
  26. const searched = ref('')
  27. const period = ref(props.period)
  28. const orderIsDesc = ref(true)
  29. const emit = defineEmits(['has-more-entries'])
  30. const visibleAuthentications = computed(() => {
  31. return authentications.value.filter(authentication => {
  32. return JSON.stringify(authentication)
  33. .toString()
  34. .toLowerCase()
  35. .includes(searched.value);
  36. })
  37. })
  38. onMounted(() => {
  39. getAuthentications()
  40. })
  41. /**
  42. * Sets the visible time span
  43. *
  44. * @param {int} duration
  45. */
  46. function setPeriod(duration) {
  47. period.value = duration
  48. getAuthentications()
  49. }
  50. /**
  51. * Set sort order to ASC
  52. */
  53. function setAsc() {
  54. orderIsDesc.value = false
  55. sortAsc()
  56. }
  57. /**
  58. * Sorts entries ascending
  59. */
  60. function sortAsc() {
  61. authentications.value.sort((a, b) => a.id > b.id ? 1 : -1)
  62. }
  63. /**
  64. * Set sort order to DESC
  65. */
  66. function setDesc() {
  67. orderIsDesc.value = true
  68. sortDesc()
  69. }
  70. /**
  71. * Sorts entries descending
  72. */
  73. function sortDesc() {
  74. authentications.value.sort((a, b) => a.id < b.id ? 1 : -1)
  75. }
  76. /**
  77. * Gets user authentication logs
  78. */
  79. function getAuthentications() {
  80. isFetching.value = true
  81. let limit = props.lastOnly ? 4 : false
  82. userService.getauthentications(props.userId, period.value, limit, {returnError: true})
  83. .then(response => {
  84. authentications.value = response.data
  85. if (authentications.value.length > 3 && props.lastOnly) {
  86. emit('has-more-entries')
  87. authentications.value.pop()
  88. }
  89. orderIsDesc.value == true ? sortDesc() : sortAsc()
  90. })
  91. .catch(error => {
  92. notify.error(error)
  93. })
  94. .finally(() => {
  95. isFetching.value = false
  96. })
  97. }
  98. const deviceIcon = (device) => {
  99. switch (device) {
  100. case "phone":
  101. return 'mobile-screen'
  102. case "tablet":
  103. return 'tablet-screen-button'
  104. default:
  105. return 'display'
  106. }
  107. }
  108. const isSuccessfulLogin = (authentication) => {
  109. return authentication.login_successful && authentication.login_at
  110. }
  111. const isSuccessfulLogout = (authentication) => {
  112. return !authentication.login_at && authentication.logout_at
  113. }
  114. const isFailedEntry = (authentication) => {
  115. return !authentication.login_successful && !authentication.logout_at
  116. }
  117. </script>
  118. <template>
  119. <SearchBox v-if="props.showSearch" v-model:keyword="searched" :hasNoBackground="true" />
  120. <nav v-if="props.showSearch" class="level is-mobile mb-2">
  121. <div class="level-item has-text-centered">
  122. <div class="buttons">
  123. <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">
  124. {{ $t('commons.one_month') }}
  125. </button>
  126. <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">
  127. {{ $t('commons.x_month', { 'x' : '3' }) }}
  128. </button>
  129. <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">
  130. {{ $t('commons.x_month', { 'x' : '6' }) }}
  131. </button>
  132. <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">
  133. {{ $t('commons.one_year') }}
  134. </button>
  135. <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">
  136. <FontAwesomeIcon :icon="['fas', 'arrow-up-long']" flip="vertical" />
  137. <FontAwesomeIcon :icon="['far', 'calendar']" />
  138. </button>
  139. <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">
  140. <FontAwesomeIcon :icon="['fas', 'arrow-up-long']" />
  141. <FontAwesomeIcon :icon="['far', 'calendar']" />
  142. </button>
  143. </div>
  144. </div>
  145. </nav>
  146. <div v-if="visibleAuthentications.length > 0">
  147. <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">
  148. <UseColorMode v-slot="{ mode }">
  149. <div>
  150. <div>
  151. <span v-if="isFailedEntry(authentication)" v-html="$t('admin.failed_login_on', { login_at: authentication.login_at })" />
  152. <span v-else-if="isSuccessfulLogout(authentication)" v-html="$t('admin.successful_logout_on', { login_at: authentication.logout_at })" />
  153. <span v-else v-html="$t('admin.successful_login_on', { login_at: authentication.login_at })" />
  154. </div>
  155. <div>
  156. {{ $t('commons.IP') }}: <span class="light-or-darker">{{ authentication.ip_address }}</span> -
  157. {{ $t('commons.browser') }}: <span class="light-or-darker">{{ authentication.browser }}</span> -
  158. {{ $t('commons.operating_system_short') }}: <span class="light-or-darker">{{ authentication.platform }}</span>
  159. </div>
  160. </div>
  161. <div :class="mode == 'dark' ? 'has-text-grey-darker' : 'has-text-grey-lighter'" class="is-align-self-center ">
  162. <font-awesome-layers class="fa-2x width-1-5x">
  163. <FontAwesomeIcon :icon="['fas', deviceIcon(authentication.device)]" transform="grow-6" fixed-width />
  164. <FontAwesomeIcon :icon="['fas', isFailedEntry(authentication) ? 'times' : 'check']"
  165. :transform="'shrink-7' + (authentication.device == 'desktop' ? ' up-2' : '')"
  166. :class="isFailedEntry(authentication) ? 'has-text-danger' + (mode == 'dark' ? '-dark' : '') : 'has-text-success' + (mode == 'dark' ? '-dark' : '')" fixed-width />
  167. </font-awesome-layers>
  168. </div>
  169. </UseColorMode>
  170. </div>
  171. </div>
  172. <div v-else-if="authentications.length == 0" class="mt-4">
  173. {{ $t('commons.no_entry_yet') }}
  174. </div>
  175. <div v-else class="mt-5 pl-3">
  176. {{ $t('commons.no_result') }}
  177. </div>
  178. <Spinner :isVisible="isFetching" />
  179. </template>