AccessLogViewer.vue 8.0 KB

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