Skip to content

Commit

Permalink
Confirmable Support (#196)
Browse files Browse the repository at this point in the history
* confirmable

* updates to confirmable

* tweak wording

* Apply fixes from StyleCI (#156)

* work on inertia confirmations

* use configured timeouts

* Apply fixes from StyleCI (#158)

* change within

* remove unused variable

* localize
  • Loading branch information
taylorotwell authored Sep 14, 2020
1 parent f3a45bd commit 9ebb099
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 34 deletions.
46 changes: 46 additions & 0 deletions resources/views/components/confirms-password.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@props(['title' => __('Confirm Password'), 'content' => __('For your security, please confirm your password to continue.'), 'button' => __('Confirm')])

@php
$confirmableId = md5($attributes->wire('then'));
@endphp

<span
{{ $attributes->wire('then') }}
x-data
x-ref="span"
x-on:click="$wire.startConfirmingPassword('{{ $confirmableId }}')"
x-on:password-confirmed.window="setTimeout(() => $event.detail.id === '{{ $confirmableId }}' && $refs.span.dispatchEvent(new CustomEvent('then', { bubbles: false })), 250);"
>
{{ $slot }}
</span>

@once
<x-jet-dialog-modal wire:model="confirmingPassword">
<x-slot name="title">
{{ $title }}
</x-slot>

<x-slot name="content">
{{ $content }}

<div class="mt-4" x-data="{}" x-on:confirming-password.window="setTimeout(() => $refs.confirmable_password.focus(), 250)">
<x-jet-input type="password" class="mt-1 block w-3/4" placeholder="Password"
x-ref="confirmable_password"
wire:model.defer="confirmablePassword"
wire:keydown.enter="confirmPassword" />

<x-jet-input-error for="confirmable_password" class="mt-2" />
</div>
</x-slot>

<x-slot name="footer">
<x-jet-secondary-button wire:click="stopConfirmingPassword" wire:loading.attr="disabled">
Nevermind
</x-jet-secondary-button>

<x-jet-button class="ml-2" wire:click="confirmPassword" wire:loading.attr="disabled">
{{ $button }}
</x-jet-button>
</x-slot>
</x-jet-dialog-modal>
@endonce
115 changes: 115 additions & 0 deletions src/ConfirmsPasswords.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace Laravel\Jetstream;

use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Actions\ConfirmPassword;

trait ConfirmsPasswords
{
/**
* Indicates if the user's password is being confirmed.
*
* @var bool
*/
public $confirmingPassword = false;

/**
* The ID of the operation being confirmed.
*
* @var string|null
*/
public $confirmableId = null;

/**
* The user's password.
*
* @var string
*/
public $confirmablePassword = '';

/**
* Start confirming the user's password.
*
* @param string $confirmableId
* @return void
*/
public function startConfirmingPassword(string $confirmableId)
{
$this->resetErrorBag();

if ($this->passwordIsConfirmed()) {
return $this->dispatchBrowserEvent('password-confirmed', [
'id' => $confirmableId,
]);
}

$this->confirmingPassword = true;
$this->confirmableId = $confirmableId;
$this->confirmablePassword = '';

$this->dispatchBrowserEvent('confirming-password');
}

/**
* Stop confirming the user's password.
*
* @return void
*/
public function stopConfirmingPassword()
{
$this->confirmingPassword = false;
$this->confirmableId = null;
$this->confirmablePassword = '';
}

/**
* Confirm the user's password.
*
* @return void
*/
public function confirmPassword()
{
if (! app(ConfirmPassword::class)(app(StatefulGuard::class), Auth::user(), $this->confirmablePassword)) {
throw ValidationException::withMessages([
'confirmable_password' => [__('This password does not match our records.')],
]);
}

session(['auth.password_confirmed_at' => time()]);

$this->dispatchBrowserEvent('password-confirmed', [
'id' => $this->confirmableId,
]);

$this->stopConfirmingPassword();
}

/**
* Ensure that the user's password has been recently confirmed.
*
* @param int|null $maximumSecondsSinceConfirmation
* @return void
*/
protected function ensurePasswordIsConfirmed($maximumSecondsSinceConfirmation = null)
{
$maximumSecondsSinceConfirmation = $maximumSecondsSinceConfirmation ?: config('auth.password_timeout', 900);

return $this->passwordIsConfirmed($maximumSecondsSinceConfirmation) ? null : abort(403);
}

/**
* Determine if the user's password has been recently confirmed.
*
* @param int|null $maximumSecondsSinceConfirmation
* @return bool
*/
protected function passwordIsConfirmed($maximumSecondsSinceConfirmation = null)
{
$maximumSecondsSinceConfirmation = $maximumSecondsSinceConfirmation ?: config('auth.password_timeout', 900);

return (time() - session('auth.password_confirmed_at', 0)) < $maximumSecondsSinceConfirmation;
}
}
21 changes: 21 additions & 0 deletions src/Http/Livewire/TwoFactorAuthenticationForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
use Laravel\Jetstream\ConfirmsPasswords;
use Livewire\Component;

class TwoFactorAuthenticationForm extends Component
{
use ConfirmsPasswords;

/**
* Indicates if two factor authentication QR code is being displayed.
*
Expand All @@ -32,12 +35,26 @@ class TwoFactorAuthenticationForm extends Component
*/
public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $enable)
{
$this->ensurePasswordIsConfirmed();

$enable(Auth::user());

$this->showingQrCode = true;
$this->showingRecoveryCodes = true;
}

/**
* Display the user's recovery codes.
*
* @return void
*/
public function showRecoveryCodes()
{
$this->ensurePasswordIsConfirmed();

$this->showingRecoveryCodes = true;
}

/**
* Generate new recovery codes for the user.
*
Expand All @@ -46,6 +63,8 @@ public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $ena
*/
public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generate)
{
$this->ensurePasswordIsConfirmed();

$generate(Auth::user());

$this->showingRecoveryCodes = true;
Expand All @@ -59,6 +78,8 @@ public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generate)
*/
public function disableTwoFactorAuthentication(DisableTwoFactorAuthentication $disable)
{
$this->ensurePasswordIsConfirmed();

$disable(Auth::user());
}

Expand Down
1 change: 1 addition & 0 deletions src/JetstreamServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ protected function configureComponents()
$this->registerComponent('authentication-card-logo');
$this->registerComponent('button');
$this->registerComponent('confirmation-modal');
$this->registerComponent('confirms-password');
$this->registerComponent('danger-button');
$this->registerComponent('dialog-modal');
$this->registerComponent('dropdown');
Expand Down
116 changes: 116 additions & 0 deletions stubs/inertia/resources/js/Jetstream/ConfirmsPassword.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<span>
<span @click="startConfirmingPassword">
<slot />
</span>

<jet-dialog-modal :show="confirmingPassword" @close="confirmingPassword = false">
<template #title>
{{ title }}
</template>

<template #content>
{{ content }}

<div class="mt-4">
<jet-input type="password" class="mt-1 block w-3/4" placeholder="Password"
ref="password"
v-model="form.password"
@keyup.enter.native="confirmPassword" />

<jet-input-error :message="form.error" class="mt-2" />
</div>
</template>

<template #footer>
<jet-secondary-button @click.native="confirmingPassword = false">
Nevermind
</jet-secondary-button>

<jet-button class="ml-2" @click.native="confirmPassword" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
{{ button }}
</jet-button>
</template>
</jet-dialog-modal>
</span>
</template>

<script>
import JetButton from './Button'
import JetDialogModal from './DialogModal'
import JetInput from './Input'
import JetInputError from './InputError'
import JetSecondaryButton from './SecondaryButton'
export default {
props: {
title: {
default: 'Confirm Password',
},
content: {
default: 'For your security, please confirm your password to continue.',
},
button: {
default: 'Confirm',
}
},
components: {
JetButton,
JetDialogModal,
JetInput,
JetInputError,
JetSecondaryButton,
},
data() {
return {
confirmingPassword: false,
form: this.$inertia.form({
password: '',
error: '',
}, {
bag: 'confirmPassword',
})
}
},
methods: {
startConfirmingPassword() {
this.form.error = '';
axios.get('/user/confirmed-password-status').then(response => {
if (response.data.confirmed) {
this.$emit('confirmed');
} else {
this.confirmingPassword = true;
this.form.password = '';
setTimeout(() => {
this.$refs.password.focus()
}, 250)
}
})
},
confirmPassword() {
this.form.processing = true;
axios.post('/user/confirm-password', {
password: this.form.password,
}).then(response => {
this.confirmingPassword = false;
this.form.password = '';
this.form.error = '';
this.form.processing = false;
this.$nextTick(() => this.$emit('confirmed'));
}).catch(error => {
this.form.processing = false;
this.form.error = error.response.data.errors.password[0];
});
}
}
}
</script>
Loading

0 comments on commit 9ebb099

Please sign in to comment.