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

View File

@ -24,7 +24,7 @@ public function rules(): array
{ {
return [ return [
'name' => ['required', 'string', 'max:50'], '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())], 'gender' => ['required', Rule::in(Gender::cases())],
]; ];
} }

View File

@ -16,7 +16,7 @@ public function up(): void
$table->id(); $table->id();
$table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete(); $table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete();
$table->string('name', 50); $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->enum('gender', Gender::values())->comment(Gender::comment());
$table->timestamp('created_at')->useCurrent(); $table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); $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>Pastikan email aktif karena akan digunakan untuk verifikasi.
</flux:description> </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" /> autocomplete="off" />
<flux:error name="form.email" /> <flux:error name="form.email" />
@ -56,7 +56,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
<flux:field> <flux:field>
<flux:label>Nama Pengguna <span class="text-red-500 ms-1">*</span></flux:label> <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:error name="form.username" />
</flux:field> </flux:field>
@ -64,7 +64,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
<flux:field> <flux:field>
<flux:label>Kata Sandi <span class="text-red-500 ms-1">*</span></flux:label> <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 /> autocomplete="off" viewable />
<flux:error name="form.password" /> <flux:error name="form.password" />
@ -77,7 +77,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
<flux:field> <flux:field>
<flux:label>Nama <span class="text-red-500 ms-1">*</span></flux:label> <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:error name="form.name" />
</flux:field> </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:label>Nomor Telepon <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="0821 xxxx xxxx" mask="9999 9999 99999" <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:error name="form.phone_number" />
</flux:field> </flux:field>
@ -94,7 +94,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
<flux:field> <flux:field>
<flux:label>Jenis Kelamin <span class="text-red-500 ms-1">*</span></flux:label> <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) @foreach (\App\Enums\Gender::cases() as $gender)
<flux:radio value="{{ $gender->value }}"> <flux:radio value="{{ $gender->value }}">
{{ $gender->label() }} {{ $gender->label() }}
@ -106,7 +106,7 @@ class="transition-all duration-500 ease-in-out {{ $currentStep === $step['id'] ?
</flux:field> </flux:field>
<flux:input label="Kode Referral" placeholder="Masukkan kode referral" <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> </div>
@endif @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\Logout as LogoutComponent;
use App\Livewire\Auth\Register as RegisterComponent; use App\Livewire\Auth\Register as RegisterComponent;
use App\Livewire\Auth\ResetPassword as ResetPasswordComponent; use App\Livewire\Auth\ResetPassword as ResetPasswordComponent;
use App\Livewire\Auth\VerifyEmail as VerifyEmailComponent;
use Illuminate\Foundation\Auth\EmailVerificationRequest; use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -22,14 +23,12 @@
Route::post('logout', LogoutComponent::class)->name('logout')->middleware('auth'); Route::post('logout', LogoutComponent::class)->name('logout')->middleware('auth');
Route::get('/email/verify', function () { Route::get('/email/verify', VerifyEmailComponent::class)->middleware('auth')->name('verification.notice');
return view('auth.verify-email');
})->middleware('auth')->name('verification.notice');
Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) { Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
$request->fulfill(); $request->fulfill();
return redirect('/'); return redirect()->intended(route('member.overview', absolute: false).'?verified=1');
})->middleware(['auth', 'signed'])->name('verification.verify'); })->middleware(['auth', 'signed'])->name('verification.verify');
Route::post('/email/verification-notification', function (Request $request) { Route::post('/email/verification-notification', function (Request $request) {

View File

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