feat: Implement email verification, protect member routes, and improve registration with debounced inputs and unique phone numbers.

This commit is contained in:
Yoga Pangestu 2026-01-04 21:39:00 +07:00
parent a9759f92c1
commit 096a7b384b
9 changed files with 108 additions and 20 deletions

View File

@ -61,7 +61,7 @@ public function auth(): void
session()->regenerate();
$this->toast('Hai, '.($user?->employee?->full_name ?? $user->name).'! Semoga harimu menyenangkan 😊');
$this->toast('Hai, '.($user?->employee?->full_name ?? $user->customer?->name).'! Semoga harimu menyenangkan 😊');
$this->redirectByRole($user->roles->pluck('name')->toArray());
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Livewire\Auth;
use App\Traits\Components\WithToast;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('components.layouts.auth', [
'title' => 'Verifikasi Email',
])]
class VerifyEmail extends Component
{
use WithToast;
/**
* Send email verification notification to the user.
*/
public function resend(): void
{
if (Auth::user()->hasVerifiedEmail()) {
$this->redirectIntended(route('member.overview'), navigate: true);
return;
}
Auth::user()->sendEmailVerificationNotification();
$this->toast('Tautan verifikasi baru telah dikirim ke alamat email Anda.');
}
/**
* Log the user out of the application.
*/
public function logout(): void
{
Auth::logout();
$this->redirect(route('login'), navigate: true);
}
public function render(): View
{
return view('livewire.auth.verify-email');
}
}

View File

@ -13,6 +13,7 @@
use App\Models\Tier;
use App\Models\User;
use App\Rules\PhoneNumber;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
@ -56,7 +57,7 @@ public function rulesStep2(): array
{
return [
'name' => ['required', 'string', 'max:50'],
'phone_number' => ['required', 'string', new PhoneNumber],
'phone_number' => ['required', 'string', new PhoneNumber, Rule::unique('customers', 'phone_number')],
'gender' => ['required', Rule::in(Gender::values())],
'referral_code' => ['nullable', 'string', Rule::exists('referral_codes', 'code')],
];
@ -99,7 +100,8 @@ public function auth(): string
$maxUser = User::max('id') + 1;
$referralCode = ReferralCode::where('code', $this->referral_code)->first();
DB::transaction(function () use ($tier, $maxUser, $referralCode, &$customer): void {
$user = null;
DB::transaction(function () use ($tier, $maxUser, $referralCode, &$user): void {
$user = User::create([
'email' => $this->email,
'username' => $this->username,
@ -107,7 +109,7 @@ public function auth(): string
'status' => UserStatus::ACTIVE,
]);
$customer = Customer::create([
Customer::create([
'user_id' => $user->id,
'name' => $this->name,
'phone_number' => $this->phone_number,
@ -144,11 +146,11 @@ public function auth(): string
$user->assignRole('Customer');
auth()->login($user);
Auth::login($user);
$user->sendEmailVerificationNotification();
});
return $customer->name;
return $this->name;
}
}

View File

@ -24,7 +24,7 @@ public function rules(): array
{
return [
'name' => ['required', 'string', 'max:50'],
'phone_number' => ['nullable', 'string', new PhoneNumber],
'phone_number' => ['nullable', 'string', new PhoneNumber, Rule::unique('customers', 'phone_number')],
'gender' => ['required', Rule::in(Gender::cases())],
];
}

View File

@ -16,7 +16,7 @@ public function up(): void
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete();
$table->string('name', 50);
$table->string('phone_number', 20)->nullable();
$table->string('phone_number', 20)->nullable()->unique();
$table->enum('gender', Gender::values())->comment(Gender::comment());
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();

View File

@ -47,7 +47,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
<flux:description>Pastikan email aktif karena akan digunakan untuk verifikasi.
</flux:description>
<flux:input placeholder="info.pangestuyoga@gmail.com" wire:model.live="form.email" autofocus
<flux:input placeholder="info.pangestuyoga@gmail.com" wire:model.live.debounce.500ms="form.email" autofocus
autocomplete="off" />
<flux:error name="form.email" />
@ -56,7 +56,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
<flux:field>
<flux:label>Nama Pengguna <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="jonhdoe" wire:model.live="form.username" autocomplete="off" />
<flux:input placeholder="jonhdoe" wire:model.live.debounce.500ms="form.username" autocomplete="off" />
<flux:error name="form.username" />
</flux:field>
@ -64,7 +64,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
<flux:field>
<flux:label>Kata Sandi <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="********" type="password" wire:model.live="form.password"
<flux:input placeholder="********" type="password" wire:model.live.debounce.500ms="form.password"
autocomplete="off" viewable />
<flux:error name="form.password" />
@ -77,7 +77,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
<flux:field>
<flux:label>Nama <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="John Doe" wire:model.live="form.name" autofocus autocomplete="off" />
<flux:input placeholder="John Doe" wire:model.live.debounce.500ms="form.name" autofocus autocomplete="off" />
<flux:error name="form.name" />
</flux:field>
@ -86,7 +86,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
<flux:label>Nomor Telepon <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="0821 xxxx xxxx" mask="9999 9999 99999"
wire:model.live="form.phone_number" autocomplete="off" />
wire:model.live.debounce.500ms="form.phone_number" autocomplete="off" />
<flux:error name="form.phone_number" />
</flux:field>
@ -94,7 +94,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
<flux:field>
<flux:label>Jenis Kelamin <span class="text-red-500 ms-1">*</span></flux:label>
<flux:radio.group wire:model.live="form.gender" variant="buttons" class="w-full *:flex-1">
<flux:radio.group wire:model.live.debounce.500ms="form.gender" variant="buttons" class="w-full *:flex-1">
@foreach (\App\Enums\Gender::cases() as $gender)
<flux:radio value="{{ $gender->value }}">
{{ $gender->label() }}
@ -106,7 +106,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
</flux:field>
<flux:input label="Kode Referral" placeholder="Masukkan kode referral"
wire:model.live="form.referral_code" autocomplete="off" />
wire:model.live.debounce.500ms="form.referral_code" autocomplete="off" />
</div>
@endif

View File

@ -0,0 +1,39 @@
<div class="flex h-screen w-full items-center justify-center bg-zinc-50 dark:bg-zinc-950 p-6">
<div class="w-full max-w-md space-y-8">
<div class="flex flex-col items-center justify-center gap-4 opacity-50">
<flux:brand href="{{ route('homepage') }}" name="Yadi Parfum" wire:navigate>
<x-slot name="logo">
<div
class="size-12 rounded shrink-0 bg-accent text-accent-foreground flex items-center justify-center">
<i class="font-serif font-bold text-xl">YD</i>
</div>
</x-slot>
</flux:brand>
<div class="text-center">
<flux:heading size="xl" class="font-semibold">Verifikasi Email Anda</flux:heading>
<flux:subheading class="mt-2">
Terima kasih telah mendaftar! Sebelum memulai, harap verifikasi email Anda dengan mengeklik tautan
yang baru saja kami kirimkan.
</flux:subheading>
</div>
</div>
<div
class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800 space-y-6">
<div class="text-sm text-zinc-600 dark:text-zinc-400">
Jika Anda tidak menerima email tersebut, kami dengan senang hati akan mengirimkan email yang baru.
</div>
<div class="flex flex-col gap-3">
<flux:button wire:click="resend" variant="primary" class="w-full">
Kirim Ulang Email Verifikasi
</flux:button>
<flux:button wire:click="logout" variant="ghost" class="w-full">
Keluar
</flux:button>
</div>
</div>
</div>
</div>

View File

@ -5,6 +5,7 @@
use App\Livewire\Auth\Logout as LogoutComponent;
use App\Livewire\Auth\Register as RegisterComponent;
use App\Livewire\Auth\ResetPassword as ResetPasswordComponent;
use App\Livewire\Auth\VerifyEmail as VerifyEmailComponent;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
@ -22,14 +23,12 @@
Route::post('logout', LogoutComponent::class)->name('logout')->middleware('auth');
Route::get('/email/verify', function () {
return view('auth.verify-email');
})->middleware('auth')->name('verification.notice');
Route::get('/email/verify', VerifyEmailComponent::class)->middleware('auth')->name('verification.notice');
Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
$request->fulfill();
return redirect('/');
return redirect()->intended(route('member.overview', absolute: false).'?verified=1');
})->middleware(['auth', 'signed'])->name('verification.verify');
Route::post('/email/verification-notification', function (Request $request) {

View File

@ -6,7 +6,7 @@
use App\Livewire\Member\Voucher;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')
Route::middleware(['auth', 'verified'])
->prefix('member')
->name('member.')
->group(function () {