mirror of https://github.com/Bubka/2FAuth.git
Compare commits
7 Commits
063fb2ef18
...
5a74a37348
Author | SHA1 | Date |
---|---|---|
Bubka | 5a74a37348 | |
Bubka | ec396b00e8 | |
Bubka | ab3240ec53 | |
Bubka | e5f6fbf431 | |
Bubka | a12a03d330 | |
Bubka | 6e41e284b5 | |
Bubka | 0e73738ee1 |
|
@ -220,7 +220,7 @@ class UserManagerController extends Controller
|
|||
'limit' => 'sometimes|numeric',
|
||||
]);
|
||||
|
||||
$authentications = $request->has('period') ? $user->authentications($validated['period'])->get() : $user->authentications->get();
|
||||
$authentications = $request->has('period') ? $user->authenticationsByPeriod($validated['period']) : $user->authentications;
|
||||
$authentications = $request->has('limit') ? $authentications->take($validated['limit']) : $authentications;
|
||||
|
||||
return UserAuthentication::collection($authentications);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Models\Traits;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Rappasoft\LaravelAuthenticationLog\Models\AuthenticationLog;
|
||||
use Rappasoft\LaravelAuthenticationLog\Traits\AuthenticationLoggable as TraitsAuthenticationLoggable;
|
||||
|
@ -10,13 +11,20 @@ trait AuthenticationLoggable
|
|||
{
|
||||
use TraitsAuthenticationLoggable;
|
||||
|
||||
public function authentications(int $period = 1)
|
||||
public function authentications()
|
||||
{
|
||||
return $this->morphMany(AuthenticationLog::class, 'authenticatable')->latest('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authentications for the provided timespan (in month)
|
||||
*/
|
||||
public function authenticationsByPeriod(int $period = 1)
|
||||
{
|
||||
$from = Carbon::now()->subMonths($period);
|
||||
|
||||
return $this->morphMany(AuthenticationLog::class, 'authenticatable')
|
||||
->where('login_at', '>=', $from)
|
||||
->orWhere('logout_at', '>=', $from)
|
||||
->orderByDesc('id');
|
||||
return $this->authentications->filter(function (AuthenticationLog $authentication) use ($from) {
|
||||
return $authentication->login_at >= $from || $authentication->logout_at >= $from;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1390,4 +1390,15 @@ footer.main .field.is-grouped {
|
|||
top: 4px;
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.light-or-darker {
|
||||
color: $grey-darker;
|
||||
}
|
||||
:root[data-theme="dark"] .light-or-darker {
|
||||
color: $grey-light;
|
||||
}
|
||||
|
||||
.width-1-5x {
|
||||
width: 1.5em;
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
import Spinner from '@/components/Spinner.vue'
|
||||
import { useNotifyStore } from '@/stores/notify'
|
||||
import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import { UseColorMode } from '@vueuse/components'
|
||||
|
||||
const notify = useNotifyStore()
|
||||
|
||||
|
@ -26,6 +27,8 @@
|
|||
const period = ref(periods.aMonth)
|
||||
const orderIsDesc = ref(true)
|
||||
|
||||
const emit = defineEmits(['has-more-entries'])
|
||||
|
||||
const visibleAuthentications = computed(() => {
|
||||
return authentications.value.filter(authentication => {
|
||||
return JSON.stringify(authentication)
|
||||
|
@ -84,11 +87,17 @@
|
|||
*/
|
||||
function getAuthentications() {
|
||||
isFetching.value = true
|
||||
let limit = props.lastOnly ? 3 : false
|
||||
let limit = props.lastOnly ? 4 : false
|
||||
|
||||
userService.getauthentications(props.userId, period.value, limit, {returnError: true})
|
||||
.then(response => {
|
||||
authentications.value = response.data
|
||||
|
||||
if (authentications.value.length > 3) {
|
||||
emit('has-more-entries')
|
||||
authentications.value.pop()
|
||||
}
|
||||
|
||||
orderIsDesc.value == true ? sortDesc() : sortAsc()
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -153,26 +162,28 @@
|
|||
</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>
|
||||
<UseColorMode v-slot="{ mode }">
|
||||
<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>
|
||||
<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="light-or-darker">{{ authentication.ip_address }}</span> -
|
||||
{{ $t('commons.browser') }}: <span class="light-or-darker">{{ authentication.browser }}</span> -
|
||||
{{ $t('commons.operating_system_short') }}: <span class="light-or-darker">{{ authentication.platform }}</span>
|
||||
</div>
|
||||
</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>
|
||||
<div :class="mode == 'dark' ? 'has-text-grey-darker' : 'has-text-grey-lighter'" class="is-align-self-center ">
|
||||
<font-awesome-layers class="fa-2x width-1-5x">
|
||||
<FontAwesomeIcon :icon="['fas', deviceIcon(authentication.device)]" transform="grow-6" fixed-width />
|
||||
<FontAwesomeIcon :icon="['fas', isFailedEntry(authentication) ? 'times' : 'check']"
|
||||
:transform="'shrink-7' + (authentication.device == 'desktop' ? ' up-2' : '')"
|
||||
:class="isFailedEntry(authentication) ? 'has-text-danger' + (mode == 'dark' ? '-dark' : '') : 'has-text-success' + (mode == 'dark' ? '-dark' : '')" fixed-width />
|
||||
</font-awesome-layers>
|
||||
</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', 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>
|
||||
</UseColorMode>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="authentications.length == 0" class="mt-4">
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script setup>
|
||||
const { copy } = useClipboard({ legacy: true })
|
||||
import { useNotifyStore } from '@/stores/notify'
|
||||
import { UseColorMode } from '@vueuse/components'
|
||||
|
||||
const notify = useNotifyStore()
|
||||
|
||||
|
@ -48,9 +47,7 @@
|
|||
<div class="block">
|
||||
{{ $t('errors.data_of_qrcode_is_not_valid_URI') }}
|
||||
</div>
|
||||
<UseColorMode v-slot="{ mode }">
|
||||
<div class="block mb-6" :class="mode == 'dark' ? 'has-text-light':'has-text-grey-dark'">{{ qrContent ? qrContent : '[' + trans('commons.nothing') + ']' }}</div>
|
||||
</UseColorMode>
|
||||
<div class="block mb-6 light-or-darker">{{ qrContent ? qrContent : '[' + trans('commons.nothing') + ']' }}</div>
|
||||
<!-- Copy to clipboard -->
|
||||
<div class="block has-text-link" v-if="qrContent">
|
||||
<button class="button is-link is-outlined is-rounded" @click.stop="copyToClipboard(qrContent)">
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import { useUserStore } from '@/stores/user'
|
||||
import { useBusStore } from '@/stores/bus'
|
||||
import { useNotifyStore } from '@/stores/notify'
|
||||
import { UseColorMode } from '@vueuse/components'
|
||||
import { useTwofaccounts } from '@/stores/twofaccounts'
|
||||
|
||||
const router = useRouter()
|
||||
|
@ -85,9 +84,7 @@
|
|||
</div>
|
||||
<!-- alternative methods -->
|
||||
<div class="column is-full">
|
||||
<UseColorMode v-slot="{ mode }">
|
||||
<div class="block" :class="mode == 'dark' ? 'has-text-light':'has-text-grey-dark'">{{ $t('twofaccounts.forms.alternative_methods') }}</div>
|
||||
</UseColorMode>
|
||||
<div class="block light-or-darker">{{ $t('twofaccounts.forms.alternative_methods') }}</div>
|
||||
<!-- upload a qr code -->
|
||||
<div class="block has-text-link" v-if="!user.preferences.useBasicQrcodeReader">
|
||||
<label role="button" tabindex="0" class="button is-link is-outlined is-rounded" ref="qrcodeInputLabel" @keyup.enter="qrcodeInputLabel.click()">
|
||||
|
|
|
@ -165,7 +165,7 @@
|
|||
<div class="ml-3">
|
||||
<UseColorMode v-slot="{ mode }">
|
||||
<!-- manage link -->
|
||||
<RouterLink :to="{ name: 'admin.manageUser', params: { userId: user.id }}" class="button is-small has-normal-radius is-pulled-right" :class="{'is-dark' : mode == 'dark'}" :title="$t('commons.manage')">
|
||||
<RouterLink :to="{ name: 'admin.manageUser', params: { userId: user.id }}" class="button is-small has-normal-radius" :class="{'is-dark' : mode == 'dark'}" :title="$t('commons.manage')">
|
||||
{{ $t('commons.manage') }}
|
||||
</RouterLink>
|
||||
</UseColorMode>
|
||||
|
|
|
@ -40,7 +40,8 @@
|
|||
<AccessLogViewer :userId="props.userId" :lastOnly="false" :showSearch="true" />
|
||||
<!-- footer -->
|
||||
<VueFooter :showButtons="true">
|
||||
<ButtonBackCloseCancel :returnTo="{ name: 'admin.manageUser', params: { userId: props.userId }}" action="close" />
|
||||
<ButtonBackCloseCancel :returnTo="{ name: 'admin.manageUser', params: { userId: props.userId }}" action="back" />
|
||||
<ButtonBackCloseCancel :returnTo="{ name: 'accounts' }" action="close" />
|
||||
</VueFooter>
|
||||
</ResponsiveWidthWrapper>
|
||||
</template>
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
const isFetching = ref(false)
|
||||
const managedUser = ref(null)
|
||||
const listUserPreferences = ref(null)
|
||||
const showFullLogLink = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
userId: [Number, String]
|
||||
|
@ -208,6 +209,9 @@
|
|||
<div v-if="managedUser.info.oauth_provider" class="notification is-dark is-size-7-mobile has-text-centered">
|
||||
{{ $t('admin.account_bound_to_x_via_oauth', { provider: managedUser.info.oauth_provider }) }}
|
||||
</div>
|
||||
<div class="block is-size-6 is-size-7-mobile has-text-grey">
|
||||
{{ $t('admin.registered_on_date', { date: managedUser.info.created_at }) }} - {{ $t('admin.last_seen_on_date', { date: managedUser.info.last_seen_at }) }}
|
||||
</div>
|
||||
<div class="block">
|
||||
<!-- otp as dot -->
|
||||
<FormCheckbox v-model="managedUser.info.is_admin" @update:model-value="val => saveAdminRole(val === true)" fieldName="is_admin" label="admin.forms.is_admin.label" help="admin.forms.is_admin.help" />
|
||||
|
@ -244,7 +248,7 @@
|
|||
<div class="list-item is-size-6 is-size-6-mobile has-text-grey is-flex is-justify-content-space-between">
|
||||
<div>
|
||||
<span class="has-text-weight-bold">{{ $t('settings.personal_access_tokens') }}</span>
|
||||
<span class="is-block is-family-primary is-size-7 is-size-7-mobile has-text-grey-dark">
|
||||
<span class="is-block is-family-primary has-text-grey-dark">
|
||||
{{ $t('admin.user_has_x_active_pat', { count: managedUser.valid_personal_access_tokens }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -262,7 +266,7 @@
|
|||
<div class="list-item is-size-6 is-size-6-mobile has-text-grey is-flex is-justify-content-space-between">
|
||||
<div>
|
||||
<span class="has-text-weight-bold">{{ $t('auth.webauthn.security_devices') }}</span>
|
||||
<span class="is-block is-size-7 is-size-7-mobile has-text-grey-dark">
|
||||
<span class="is-block has-text-grey-dark">
|
||||
{{ $t('admin.user_has_x_security_devices', { count: managedUser.webauthn_credentials }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -278,6 +282,16 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h3 class="title is-5 has-text-grey-light mb-2">{{ $t('admin.last_accesses') }}</h3>
|
||||
<AccessLogViewer :userId="props.userId" :lastOnly="true" @has-more-entries="showFullLogLink = true"/>
|
||||
</div>
|
||||
<div v-if="showFullLogLink" class="block is-size-6 is-size-7-mobile has-text-grey">
|
||||
{{ $t('admin.access_log_has_more_entries') }} <router-link id="lnkFullLogs" :to="{ name: 'admin.logs.access', params: { userId: props.userId }}" >
|
||||
{{ $t('admin.see_full_log') }}.
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- preferences -->
|
||||
<h2 class="title is-4 has-text-grey-light">{{ $t('settings.preferences') }}</h2>
|
||||
<div class="about-debug box is-family-monospace is-size-7">
|
||||
<CopyButton id="btnCopyEnvVars" :token="listUserPreferences?.innerText" />
|
||||
|
@ -287,20 +301,6 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- logs -->
|
||||
<h2 class="title is-4 has-text-grey-light">{{ $t('admin.logs') }}</h2>
|
||||
<div class="block is-size-6 is-size-7-mobile has-text-grey">
|
||||
{{ $t('admin.registered_on_date', { date: managedUser.info.created_at }) }} - {{ $t('admin.last_seen_on_date', { date: managedUser.info.last_seen_at }) }}
|
||||
</div>
|
||||
<div class="block">
|
||||
<h3 class="title is-6 has-text-grey-light mb-0">{{ $t('admin.last_accesses') }}</h3>
|
||||
<AccessLogViewer :userId="props.userId" :lastOnly="true" />
|
||||
</div>
|
||||
<div class="block is-size-6 is-size-7-mobile has-text-grey">
|
||||
{{ $t('admin.access_log_has_more_entries') }} <router-link id="lnkFullLogs" :to="{ name: 'admin.logs.access', params: { userId: props.userId }}" >
|
||||
{{ $t('admin.see_full_log') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- danger zone -->
|
||||
<h2 class="title is-4 has-text-danger">{{ $t('admin.danger_zone') }}</h2>
|
||||
<div class="is-left-bordered-danger">
|
||||
|
@ -318,6 +318,7 @@
|
|||
<!-- footer -->
|
||||
<VueFooter :showButtons="true">
|
||||
<ButtonBackCloseCancel :returnTo="{ name: 'admin.users' }" action="back" />
|
||||
<ButtonBackCloseCancel :returnTo="{ name: 'accounts' }" action="close" />
|
||||
</VueFooter>
|
||||
</ResponsiveWidthWrapper>
|
||||
</template>
|
||||
|
|
|
@ -68,13 +68,13 @@ return [
|
|||
'check_now' => 'Check now',
|
||||
'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>',
|
||||
'successful_login_on' => 'Successful login on <span class="light-or-darker">:login_at</span>',
|
||||
'successful_logout_on' => 'Successful logout on <span class="light-or-darker">:login_at</span>',
|
||||
'failed_login_on' => 'Failed login on <span class="light-or-darker">: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_has_more_entries' => 'The access log contains 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',
|
||||
|
|
|
@ -90,5 +90,5 @@ return [
|
|||
'device' => 'Device',
|
||||
'one_month' => '1 mo.',
|
||||
'x_month' => ':x mos.',
|
||||
'one_year' => '1 yr',
|
||||
'one_year' => '1 yr.',
|
||||
];
|
||||
|
|
|
@ -10,6 +10,7 @@ use Database\Factories\UserFactory;
|
|||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
@ -19,6 +20,8 @@ use Illuminate\Support\Str;
|
|||
use Laravel\Passport\TokenRepository;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Tests\Data\AuthenticationLogData;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
#[CoversClass(UserManagerController::class)]
|
||||
|
@ -518,4 +521,278 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
])
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
protected function feedAuthenticationLog() : int
|
||||
{
|
||||
// Do not change creation order
|
||||
$this->user->authentications()->create(AuthenticationLogData::beforeLastYear());
|
||||
$this->user->authentications()->create(AuthenticationLogData::duringLastYear());
|
||||
$this->user->authentications()->create(AuthenticationLogData::duringLastSixMonth());
|
||||
$this->user->authentications()->create(AuthenticationLogData::duringLastThreeMonth());
|
||||
$this->user->authentications()->create(AuthenticationLogData::duringLastMonth());
|
||||
$this->user->authentications()->create(AuthenticationLogData::noLogin());
|
||||
$this->user->authentications()->create(AuthenticationLogData::noLogout());
|
||||
|
||||
return 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_all_entries() : void
|
||||
{
|
||||
$created = $this->feedAuthenticationLog();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications')
|
||||
->assertOk()
|
||||
->assertJsonCount($created);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_expected_resource() : void
|
||||
{
|
||||
$this->user->authentications()->create(AuthenticationLogData::duringLastMonth());
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications')
|
||||
->assertJsonStructure([
|
||||
'*' => [
|
||||
'id',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'browser',
|
||||
'platform',
|
||||
'device',
|
||||
'login_at',
|
||||
'logout_at',
|
||||
'login_successful',
|
||||
'duration',
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_no_login_entry() : void
|
||||
{
|
||||
$this->user->authentications()->create(AuthenticationLogData::noLogin());
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1')
|
||||
->assertJsonCount(1)
|
||||
->assertJsonFragment([
|
||||
'login_at' => null
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_no_logout_entry() : void
|
||||
{
|
||||
$this->user->authentications()->create(AuthenticationLogData::noLogout());
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1')
|
||||
->assertJsonCount(1)
|
||||
->assertJsonFragment([
|
||||
'logout_at' => null
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_failed_entry() : void
|
||||
{
|
||||
$this->user->authentications()->create(AuthenticationLogData::failedLogin());
|
||||
$expected = Carbon::parse(AuthenticationLogData::failedLogin()['login_at'])->toDayDateTimeString();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1')
|
||||
->assertJsonCount(1)
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expected,
|
||||
'login_successful' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_last_month_entries() : void
|
||||
{
|
||||
$this->feedAuthenticationLog();
|
||||
$expected = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1')
|
||||
->assertJsonCount(3)
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expected
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_last_three_months_entries() : void
|
||||
{
|
||||
$this->feedAuthenticationLog();
|
||||
$expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=3')
|
||||
->assertJsonCount(4)
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedOneMonth
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedThreeMonth
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_last_six_months_entries() : void
|
||||
{
|
||||
$this->feedAuthenticationLog();
|
||||
$expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedSixMonth = Carbon::parse(AuthenticationLogData::duringLastSixMonth()['login_at'])->toDayDateTimeString();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=6')
|
||||
->assertJsonCount(5)
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedOneMonth
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedThreeMonth
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedSixMonth
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_last_year_entries() : void
|
||||
{
|
||||
$this->feedAuthenticationLog();
|
||||
$expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedSixMonth = Carbon::parse(AuthenticationLogData::duringLastSixMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedYear = Carbon::parse(AuthenticationLogData::duringLastYear()['login_at'])->toDayDateTimeString();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=12')
|
||||
->assertJsonCount(6)
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedOneMonth
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedThreeMonth
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedSixMonth
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedYear
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
#[DataProvider('LimitProvider')]
|
||||
public function test_authentications_returns_limited_entries($limit) : void
|
||||
{
|
||||
$this->feedAuthenticationLog();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?limit=' . $limit)
|
||||
->assertOk()
|
||||
->assertJsonCount($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide various limit
|
||||
*/
|
||||
public static function LimitProvider()
|
||||
{
|
||||
return [
|
||||
'limited to 1' => [1],
|
||||
'limited to 2' => [2],
|
||||
'limited to 3' => [3],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_expected_ip_and_useragent_chunks() : void
|
||||
{
|
||||
$this->user->authentications()->create([
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||
'login_at' => now(),
|
||||
'login_successful' => true,
|
||||
'logout_at' => null,
|
||||
'location' => null,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1')
|
||||
->assertJsonFragment([
|
||||
'ip_address' => '127.0.0.1',
|
||||
'browser' => 'Firefox',
|
||||
'platform' => 'Windows',
|
||||
'device' => 'desktop',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
#[DataProvider('invalidQueryParameterProvider')]
|
||||
public function test_authentications_with_invalid_limit_returns_validation_error($limit) : void
|
||||
{
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?limit=' . $limit)
|
||||
->assertStatus(422);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
#[DataProvider('invalidQueryParameterProvider')]
|
||||
public function test_authentications_with_invalid_period_returns_validation_error($period) : void
|
||||
{
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=' . $period)
|
||||
->assertStatus(422);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide various invalid value to test query parameter
|
||||
*/
|
||||
public static function invalidQueryParameterProvider()
|
||||
{
|
||||
return [
|
||||
'empty' => [''],
|
||||
'null' => ['null'],
|
||||
'boolean' => ['true'],
|
||||
'string' => ['string'],
|
||||
'array' => ['[]'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Data;
|
||||
|
||||
class AuthenticationLogData
|
||||
{
|
||||
/**
|
||||
* Indicate that the model should have login date.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function failedLogin()
|
||||
{
|
||||
$loginDate = now()->subDays(15);
|
||||
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => false,
|
||||
'logout_at' => null,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have no login date
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function noLogin()
|
||||
{
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => null,
|
||||
'login_successful' => false,
|
||||
'logout_at' => now(),
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have no logout date
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function noLogout()
|
||||
{
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => now(),
|
||||
'login_successful' => true,
|
||||
'logout_at' => null,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have login during last month
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function duringLastMonth()
|
||||
{
|
||||
$loginDate = now()->subDays(15);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => true,
|
||||
'logout_at' => $logoutDate,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have login during last 3 month
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function duringLastThreeMonth()
|
||||
{
|
||||
$loginDate = now()->subMonths(2);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => true,
|
||||
'logout_at' => $logoutDate,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have login during last 6 month
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function duringLastSixMonth()
|
||||
{
|
||||
$loginDate = now()->subMonths(4);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => true,
|
||||
'logout_at' => $logoutDate,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have login during last month
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function duringLastYear()
|
||||
{
|
||||
$loginDate = now()->subMonths(10);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => true,
|
||||
'logout_at' => $logoutDate,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have login during last month
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function beforeLastYear()
|
||||
{
|
||||
$loginDate = now()->subYears(2);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => true,
|
||||
'logout_at' => $logoutDate,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue