mirror of https://github.com/Bubka/2FAuth.git
Compare commits
2 Commits
a6745c28a6
...
44d7328d6c
Author | SHA1 | Date |
---|---|---|
Bubka | 44d7328d6c | |
Bubka | 4f17e2aff0 |
|
@ -216,10 +216,12 @@ class UserManagerController extends Controller
|
|||
$this->authorize('view', $user);
|
||||
|
||||
$validated = $this->validate($request, [
|
||||
'period' => 'sometimes|numeric',
|
||||
'limit' => 'sometimes|numeric',
|
||||
]);
|
||||
|
||||
$authentications = $request->has('limit') ? $user->authentications->take($validated['limit']) : $user->authentications;
|
||||
$authentications = $request->has('period') ? $user->authentications($validated['period'])->get() : $user->authentications->get();
|
||||
$authentications = $request->has('limit') ? $authentications->take($validated['limit']) : $authentications;
|
||||
|
||||
return UserAuthentication::collection($authentications);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,12 @@ class UserAuthentication extends JsonResource
|
|||
'browser' => $this->agent->browser(),
|
||||
'platform' => $this->agent->platform(),
|
||||
'device' => $this->agent->deviceType(),
|
||||
'login_at' => Carbon::parse($this->login_at)->toDayDateTimeString(),
|
||||
'login_at' => $this->login_at
|
||||
? Carbon::parse($this->login_at)->toDayDateTimeString()
|
||||
: null,
|
||||
'logout_at' => $this->logout_at
|
||||
? Carbon::parse($this->logout_at)->toDayDateTimeString()
|
||||
: null,
|
||||
'login_successful' => $this->login_successful,
|
||||
'duration' => $this->logout_at
|
||||
? Carbon::parse($this->logout_at)->diffForHumans(Carbon::parse($this->login_at), ['syntax' => CarbonInterface::DIFF_ABSOLUTE])
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models\Traits;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Rappasoft\LaravelAuthenticationLog\Models\AuthenticationLog;
|
||||
use Rappasoft\LaravelAuthenticationLog\Traits\AuthenticationLoggable as TraitsAuthenticationLoggable;
|
||||
|
||||
trait AuthenticationLoggable
|
||||
{
|
||||
use TraitsAuthenticationLoggable;
|
||||
|
||||
public function authentications(int $period = 1)
|
||||
{
|
||||
$from = Carbon::now()->subMonths($period);
|
||||
|
||||
return $this->morphMany(AuthenticationLog::class, 'authenticatable')
|
||||
->where('login_at', '>=', $from)
|
||||
->orWhere('logout_at', '>=', $from)
|
||||
->orderByDesc('id');
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Traits\AuthenticationLoggable;
|
||||
use App\Models\Traits\WebAuthnManageCredentials;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
|
@ -12,7 +13,6 @@ use Illuminate\Support\Facades\Hash;
|
|||
use Illuminate\Support\Str;
|
||||
use Laragear\WebAuthn\WebAuthnAuthentication;
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
use Rappasoft\LaravelAuthenticationLog\Traits\AuthenticationLoggable;
|
||||
|
||||
/**
|
||||
* App\Models\User
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script setup>
|
||||
import SearchBox from '@/components/SearchBox.vue'
|
||||
import { useNotifyStore } from '@/stores/notify'
|
||||
import userService from '@/services/userService'
|
||||
import Spinner from '@/components/Spinner.vue'
|
||||
import { useNotifyStore } from '@/stores/notify'
|
||||
import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
const notify = useNotifyStore()
|
||||
|
@ -12,9 +13,18 @@
|
|||
showSearch: Boolean
|
||||
})
|
||||
|
||||
const periods = {
|
||||
aMonth: 1,
|
||||
threeMonths: 3,
|
||||
halfYear: 6,
|
||||
aYear: 12
|
||||
}
|
||||
|
||||
const authentications = ref([])
|
||||
const isFetching = ref(false)
|
||||
const searched = ref('')
|
||||
const period = ref(periods.aMonth)
|
||||
const orderIsDesc = ref(true)
|
||||
|
||||
const visibleAuthentications = computed(() => {
|
||||
return authentications.value.filter(authentication => {
|
||||
|
@ -29,6 +39,46 @@
|
|||
getAuthentications()
|
||||
})
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user authentication logs
|
||||
*/
|
||||
|
@ -36,9 +86,10 @@
|
|||
isFetching.value = true
|
||||
let limit = props.lastOnly ? 3 : false
|
||||
|
||||
userService.getauthentications(props.userId, limit, {returnError: true})
|
||||
userService.getauthentications(props.userId, period.value, limit, {returnError: true})
|
||||
.then(response => {
|
||||
authentications.value = response.data
|
||||
orderIsDesc.value == true ? sortDesc() : sortAsc()
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error(error)
|
||||
|
@ -58,25 +109,60 @@
|
|||
return 'display'
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SearchBox v-if="props.showSearch" v-model:keyword="searched" :hasNoBackground="true" />
|
||||
<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>
|
||||
<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>
|
||||
<div >
|
||||
<span v-if="authentication.login_successful" v-html="$t('admin.successful_login_on', { login_at: authentication.login_at })" />
|
||||
<span v-else v-html="$t('admin.failed_login_on', { login_at: authentication.login_at })" />
|
||||
<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 })" />
|
||||
</div>
|
||||
<div>
|
||||
{{ $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>
|
||||
{{ $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>
|
||||
</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 />
|
||||
<FontAwesomeIcon :icon="['fas', authentication.login_successful ? 'check' : 'times']" :transform="'shrink-7' + (authentication.device == 'desktop' ? ' up-2' : '')" fixed-width :class="authentication.login_successful ? 'has-text-success-dark' : 'has-text-danger-dark'" />
|
||||
<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 />
|
||||
</font-awesome-layers>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -87,4 +173,5 @@
|
|||
<div v-else class="mt-5 pl-3">
|
||||
{{ $t('commons.no_result') }}
|
||||
</div>
|
||||
<Spinner :isVisible="isFetching" />
|
||||
</template>
|
|
@ -51,11 +51,13 @@ import {
|
|||
faMobileScreen,
|
||||
faTabletScreenButton,
|
||||
faDisplay,
|
||||
faArrowUpLong,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
faStar,
|
||||
faPaperPlane,
|
||||
faCalendar
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
import {
|
||||
|
@ -116,7 +118,9 @@ library.add(
|
|||
faAlignLeft,
|
||||
faMobileScreen,
|
||||
faTabletScreenButton,
|
||||
faDisplay
|
||||
faDisplay,
|
||||
faCalendar,
|
||||
faArrowUpLong
|
||||
);
|
||||
|
||||
export default FontAwesomeIcon
|
|
@ -134,8 +134,8 @@ export default {
|
|||
*
|
||||
* @returns promise
|
||||
*/
|
||||
getauthentications(id, limit, config = {}) {
|
||||
return apiClient.get('/users/' + id + '/authentications' + (limit ? '?limit=' + limit : ''), { ...config })
|
||||
getauthentications(id, period = 90, limit, config = {}) {
|
||||
return apiClient.get('/users/' + id + '/authentications?period=' + period + (limit ? '&limit=' + limit : ''), { ...config })
|
||||
},
|
||||
|
||||
}
|
|
@ -69,12 +69,19 @@ return [
|
|||
'view_on_github' => 'View on Github',
|
||||
'x_is_available' => ':version is available',
|
||||
'successful_login_on' => 'Successful login on <span class="has-text-grey-light">:login_at</span>',
|
||||
'successful_logout_on' => 'Successful logout on <span class="has-text-grey-light">:login_at</span>',
|
||||
'failed_login_on' => 'Failed login on <span class="has-text-grey-light">:login_at</span>',
|
||||
'last_accesses' => 'Last accesses',
|
||||
'see_full_log' => 'See full log',
|
||||
'browser_on_platform' => ':browser on :platform',
|
||||
'access_log_has_more_entries' => 'The access log is likely to contain more entries.',
|
||||
'access_log_legend_for_user' => 'Full access log for user :username',
|
||||
'show_last_month_log' => 'Show entries from the last month',
|
||||
'show_three_months_log' => 'Show entries from the last 3 months',
|
||||
'show_six_months_log' => 'Show entries from the last 6 months',
|
||||
'show_one_year_log' => 'Show entries from the last year',
|
||||
'sort_by_date_asc' => 'Show least recent first',
|
||||
'sort_by_date_desc' => 'Show most recent first',
|
||||
'forms' => [
|
||||
'use_encryption' => [
|
||||
'label' => 'Protect sensitive data',
|
||||
|
|
Loading…
Reference in New Issue