Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Confirmable Support #196

Merged
merged 10 commits into from
Sep 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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