Skip to content

Commit

Permalink
feat: add verification email resend facility, throttling, minor tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
pridit committed Nov 19, 2024
1 parent d355473 commit 3dce018
Show file tree
Hide file tree
Showing 17 changed files with 132 additions and 22 deletions.
6 changes: 3 additions & 3 deletions app/Events/Registered.php → app/Events/UserVerifyEmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class Registered
class UserVerifyEmail
{
use Dispatchable, InteractsWithSockets, SerializesModels;

Expand All @@ -16,6 +16,6 @@ class Registered
*/
public function __construct(
public User $user,
) {
}
public string $email,
) {}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/Auth/DeleteUserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class DeleteUserController extends Controller
public function create(Request $request): RedirectResponse
{
RateLimiter::attempt(
sprintf('delete-account:%d', auth()->user()->id),
sprintf('delete-account:%d', $request->user()->id),
1,
function () use ($request) {
if (! $request->user()->hasVerifiedEmail()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class EmailVerificationNotificationController extends Controller
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
if ($request->user()->hasVerifiedEmail() && $request->user()->getPendingEmail() === null) {
return redirect()->intended(route('home', absolute: false));
}

Expand Down
3 changes: 0 additions & 3 deletions app/Http/Controllers/Auth/RegisteredUserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace App\Http\Controllers\Auth;

use App\Events\Registered;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUserRequest;
use App\Models\User;
Expand Down Expand Up @@ -31,8 +30,6 @@ function () use ($request) {
'password' => Hash::make($request->password),
]);

event(new Registered($user));

Auth::login($user);
},
600
Expand Down
21 changes: 21 additions & 0 deletions app/Http/Controllers/Auth/ResendVerificationEmailController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Http\Controllers\Auth;

use App\Events\UserVerifyEmail;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class ResendVerificationEmailController extends Controller
{
/**
* Resend email verification email.
*/
public function __invoke(Request $request): RedirectResponse
{
event(new UserVerifyEmail($request->user(), $request->user()->email));

return redirect(route('user.setting.account', absolute: false));
}
}
17 changes: 16 additions & 1 deletion app/Http/Controllers/ProfileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Controllers;

use App\Events\UserVerifyEmail;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;

Expand All @@ -13,7 +14,21 @@ class ProfileController extends Controller
public function update(ProfileUpdateRequest $request): RedirectResponse
{
if ($request->safe()->only('email')) {
$request->user()->newEmail($request->safe()->email);
if ($request->user()->is_verification_email_throttled) {
$request->session()
->flash(
'message',
['error', 'Please wait before attempting so soon']
);

return back();
}

event(new UserVerifyEmail($request->user(), $request->safe()->email));

if (! $request->user()->hasVerifiedEmail()) {
$request->user()->update(['email' => $request->safe()->email]);
}
}

return redirect(route('user.setting.account', absolute: false));
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/UserSettingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class UserSettingController extends SettingController
public function showAccount(Request $request): Response
{
$request->user()->load('preferences');
$request->user()->append('is_verification_email_throttled');

return Inertia::render('Setting/Account', [
'preferences' => Preference::get(),
Expand Down
20 changes: 16 additions & 4 deletions app/Listeners/SendEmailVerificationNotification.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

namespace App\Listeners;

use App\Events\Registered;
use App\Events\UserVerifyEmail;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\RateLimiter;

class SendEmailVerificationNotification
{
Expand All @@ -15,10 +16,21 @@ public function __construct() {}
/**
* Handle the event.
*/
public function handle(Registered $event): void
public function handle(UserVerifyEmail $event): void
{
if ($event->user instanceof MustVerifyEmail && ! $event->user->hasVerifiedEmail()) {
$event->user->newEmail($event->user->email);
if ($event->user instanceof MustVerifyEmail) {
RateLimiter::attempt(
sprintf('verification-email:%d', $event->user->id),
1,
function () use ($event) {
if ($event->email !== $event->user->email) {
$event->user->newEmail($event->email);
} else {
$event->user->resendPendingEmailVerificationMail();
}
},
600
);
}
}
}
13 changes: 13 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ protected function isDeletionThrottled(): Attribute
);
}

/**
* Determine whether the user has recently been sent a verification email.
*/
protected function isVerificationEmailThrottled(): Attribute
{
return new Attribute(
get: fn (mixed $value, array $attributes) => RateLimiter::tooManyAttempts(
sprintf('verification-email:%d', $attributes['id']),
1
)
);
}

/**
* Get the user's Gravatar URL (if applicable)
*/
Expand Down
4 changes: 4 additions & 0 deletions app/Observers/UserObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class UserObserver
public function created(User $user): void
{
$user->assignRole('member');

if (! $user->hasVerifiedEmail()) {
$user->newEmail($user->email);
}
}

/**
Expand Down
52 changes: 48 additions & 4 deletions resources/js/Components/forms/complex/UpdateEmailForm.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script setup>
import { BaseButton } from '@/Components/base';
import { FormInput, FormResponse } from '@/Components/forms/elements';
import { useForm } from '@inertiajs/vue3';
import { faRotateRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { router, useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
const props = defineProps({
Expand All @@ -24,6 +26,14 @@ const submit = () => {
onSuccess: () => form.reset('email'),
});
};
const spinner = ref(false);
const resend = () => {
spinner.value = true;
setTimeout(() => router.get('/resend-email'), 1800);
};
</script>

<template>
Expand All @@ -44,8 +54,42 @@ const submit = () => {
$page.props.auth.user.email_verified_at !== null
? 'V'
: 'Unv'
}}erified</span
>
}}erified
<span
v-if="
$page.props.auth.user.email_verified_at === null
"
class="ml-1 cursor-pointer border-l border-warning pl-1"
:class="{
'!cursor-not-allowed':
$page.props.auth.user
.is_verification_email_throttled,
}"
:title="
(!$page.props.auth.user
.is_verification_email_throttled &&
'Resend verification email') ||
'Please wait'
"
v-on="
!$page.props.auth.user
.is_verification_email_throttled
? { click: resend }
: {}
"
>
<FontAwesomeIcon
:class="{
'fa-spin': spinner,
'opacity-40':
$page.props.auth.user
.is_verification_email_throttled,
}"
:icon="faRotateRight"
transform="shrink-2"
/>
</span>
</span>
</div>

<FormInput
Expand All @@ -69,7 +113,7 @@ const submit = () => {
? message
: [
'warning',
'You have been sent a verification link',
'Check your email for a verification link',
]
"
/>
Expand Down
1 change: 0 additions & 1 deletion resources/js/Components/forms/elements/FormResponse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ function messageType(type) {
<FontAwesomeIcon
class="mr-0.5 !align-middle"
:icon="messageType(message[0])[1]"
size="sm"
fixed-width
/>

Expand Down
2 changes: 1 addition & 1 deletion resources/js/Components/tables/CharactersTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ function getMovementRank(rank) {
<template v-else>
<FontAwesomeIcon
v-if="character.is_muted"
class="ml-2 !align-middle text-warning/75"
class="ml-2 !align-middle text-warning"
:icon="faTriangleExclamation"
size="lg"
title="This player is chat banned"
Expand Down
3 changes: 1 addition & 2 deletions resources/js/Pages/Setting/Delete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ const deleteUserRequest = () => {
<span class="ml-1 block">
I understand this action is
<span class="text-error">irreversible</span> and that my
account <span class="text-warning">cannot</span> be
restored.
account cannot be restored.
<span class="font-bold">Continue anyway</span>.
</span>
</FormCheckbox>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">

<title inertia>{{ config('app.name', 'Laravel') }}</title>

Expand Down
4 changes: 4 additions & 0 deletions routes/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\ResendVerificationEmailController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
use Illuminate\Support\Facades\Route;
Expand Down Expand Up @@ -34,6 +35,9 @@
->middleware(['signed', 'throttle:6,1'])
->name('verification.verify');

Route::get('resend-email', ResendVerificationEmailController::class)
->name('verification.resend');

Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
->name('password.confirm');

Expand Down
1 change: 1 addition & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default {
'base-100': '#262626',
'error': colors.red['500'],
'success': colors.green['600'],
'warning': colors.yellow['600'],
},
},
],
Expand Down

0 comments on commit 3dce018

Please sign in to comment.