refactor(auth): menggunakan class form, menyesuaikan view dan test atas pemindahan tersebut

This commit is contained in:
Yoga Pangestu 2025-12-08 15:16:07 +07:00
parent 13dfa28d6e
commit 8dcc57673d
13 changed files with 134 additions and 80 deletions

View File

@ -2,10 +2,11 @@
namespace App\Livewire\Auth; namespace App\Livewire\Auth;
use App\Livewire\Forms\Auth\ForgotPasswordForm;
use App\Traits\WithToast; use App\Traits\WithToast;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
#[Layout('components.layouts.auth', [ #[Layout('components.layouts.auth', [
@ -15,15 +16,14 @@ class ForgotPassword extends Component
{ {
use WithToast; use WithToast;
#[Validate(['required', 'string'])] public ForgotPasswordForm $form;
public string $email = '';
public function sendLink() public function sendLink(): mixed
{ {
$this->validate(); $this->form->validate();
$status = Password::sendResetLink( $status = Password::sendResetLink(
$this->only('email') ['form.email' => $this->form->email]
); );
return $status === Password::ResetLinkSent return $status === Password::ResetLinkSent
@ -31,7 +31,7 @@ public function sendLink()
: $this->toast(__($status), 'Gagal', 'danger'); : $this->toast(__($status), 'Gagal', 'danger');
} }
public function render() public function render(): View
{ {
return view('livewire.auth.forgot-password'); return view('livewire.auth.forgot-password');
} }

View File

@ -3,9 +3,12 @@
namespace App\Livewire\Auth; namespace App\Livewire\Auth;
use App\Enums\UserStatus; use App\Enums\UserStatus;
use App\Livewire\Forms\Auth\LoginForm;
use App\Models\User;
use App\Traits\WithToast; use App\Traits\WithToast;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
#[Layout('components.layouts.auth', [ #[Layout('components.layouts.auth', [
@ -15,13 +18,9 @@ class Login extends Component
{ {
use WithToast; use WithToast;
#[Validate(['required', 'string'], attribute: 'nama pengguna atau email')] public LoginForm $form;
public string $login = '';
#[Validate('required', attribute: 'kata sandi')] public function mount(): void
public string $password = '';
public function mount()
{ {
if (! request()->has('message')) { if (! request()->has('message')) {
return; return;
@ -36,9 +35,9 @@ public function mount()
JS); JS);
} }
public function auth() public function auth(): void
{ {
$this->validate(); $this->form->validate();
if (! $this->attemptLogin()) { if (! $this->attemptLogin()) {
$this->toast('Oops! Nama pengguna, email, atau kata sandi tidak sesuai.', 'Gagal', 'danger'); $this->toast('Oops! Nama pengguna, email, atau kata sandi tidak sesuai.', 'Gagal', 'danger');
@ -49,7 +48,7 @@ public function auth()
$user = auth()->user(); $user = auth()->user();
if (! $this->userIsActive($user)) { if (! $this->userIsActive($user)) {
auth()->logout(); Auth::logout();
$this->toast( $this->toast(
'Akun Anda belum aktif. Mohon periksa email untuk mengaktifkan akun, dan hubungi kami jika memerlukan bantuan lebih lanjut.', 'Akun Anda belum aktif. Mohon periksa email untuk mengaktifkan akun, dan hubungi kami jika memerlukan bantuan lebih lanjut.',
@ -67,22 +66,22 @@ public function auth()
$this->redirectByRole($user->roles->pluck('name')->toArray()); $this->redirectByRole($user->roles->pluck('name')->toArray());
} }
protected function attemptLogin() protected function attemptLogin(): bool
{ {
$field = filter_var($this->login, FILTER_VALIDATE_EMAIL) ? 'email' : 'username'; $field = filter_var($this->form->login, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
return auth()->attempt([ return Auth::attempt([
$field => $this->login, $field => $this->form->login,
'password' => $this->password, 'password' => $this->form->password,
]); ]);
} }
protected function userIsActive($user) protected function userIsActive(?User $user): bool
{ {
return $user?->status === UserStatus::ACTIVE; return $user?->status === UserStatus::ACTIVE;
} }
protected function redirectByRole(array $roles) protected function redirectByRole(array $roles): void
{ {
if (array_intersect($roles, ['Developer', 'Owner', 'Leader', 'Admin'])) { if (array_intersect($roles, ['Developer', 'Owner', 'Leader', 'Admin'])) {
$this->redirectIntended('/studio/dashboard/overview', navigate: true); $this->redirectIntended('/studio/dashboard/overview', navigate: true);
@ -105,7 +104,7 @@ protected function redirectByRole(array $roles)
$this->redirectIntended('/login', navigate: true); $this->redirectIntended('/login', navigate: true);
} }
public function render() public function render(): View
{ {
return view('livewire.auth.login'); return view('livewire.auth.login');
} }

View File

@ -2,11 +2,12 @@
namespace App\Livewire\Auth; namespace App\Livewire\Auth;
use Illuminate\View\View;
use Livewire\Component; use Livewire\Component;
class Logout extends Component class Logout extends Component
{ {
public function logout() public function logout(): void
{ {
auth()->logout(); auth()->logout();
@ -16,7 +17,7 @@ public function logout()
$this->redirectRoute('login'); $this->redirectRoute('login');
} }
public function render() public function render(): View
{ {
return view('livewire.auth.logout'); return view('livewire.auth.logout');
} }

View File

@ -9,6 +9,7 @@
use App\Traits\WithToast; use App\Traits\WithToast;
use App\Traits\WithUpdatedData; use App\Traits\WithUpdatedData;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\View\View;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Component; use Livewire\Component;
@ -27,7 +28,7 @@ class Register extends Component
public int $totalSteps = 2; public int $totalSteps = 2;
public function mount() public function mount(): void
{ {
$this->steps = [ $this->steps = [
['id' => 1, 'name' => 'Kredensial'], ['id' => 1, 'name' => 'Kredensial'],
@ -35,7 +36,7 @@ public function mount()
]; ];
} }
public function updateStep(int $value) public function updateStep(int $value): void
{ {
if ($this->shouldValidateBeforeChangingStep()) { if ($this->shouldValidateBeforeChangingStep()) {
$this->form->validateStep1(); $this->form->validateStep1();
@ -44,7 +45,7 @@ public function updateStep(int $value)
$this->currentStep = $value; $this->currentStep = $value;
} }
public function nextStep() public function nextStep(): void
{ {
match ($this->currentStep) { match ($this->currentStep) {
1 => $this->form->validateStep1(), 1 => $this->form->validateStep1(),
@ -57,19 +58,19 @@ public function nextStep()
} }
} }
public function prevStep() public function prevStep(): void
{ {
if ($this->currentStep > 1) { if ($this->currentStep > 1) {
$this->currentStep--; $this->currentStep--;
} }
} }
public function updated(string $propertyName, $value) public function updated(string $propertyName, mixed $value): void
{ {
$this->validateUpdatedField($propertyName); $this->validateUpdatedField($propertyName);
} }
public function register() public function register(): void
{ {
$this->form->auth(); $this->form->auth();
@ -78,7 +79,7 @@ public function register()
$this->redirectAfterRegistration(); $this->redirectAfterRegistration();
} }
public function render() public function render(): View
{ {
return view('livewire.auth.register'); return view('livewire.auth.register');
} }
@ -86,7 +87,7 @@ public function render()
/** /**
* Determine whether validation is required before changing step. * Determine whether validation is required before changing step.
*/ */
protected function shouldValidateBeforeChangingStep() protected function shouldValidateBeforeChangingStep(): bool
{ {
return $this->currentStep === 1; return $this->currentStep === 1;
} }
@ -94,7 +95,7 @@ protected function shouldValidateBeforeChangingStep()
/** /**
* Validate updated field based on current form step rules. * Validate updated field based on current form step rules.
*/ */
protected function validateUpdatedField(string $propertyName) protected function validateUpdatedField(string $propertyName): void
{ {
$field = Str::replace('form.', '', $propertyName); $field = Str::replace('form.', '', $propertyName);
@ -110,7 +111,7 @@ protected function validateUpdatedField(string $propertyName)
/** /**
* Show success toast after registration. * Show success toast after registration.
*/ */
protected function sendRegistrationSuccessToast() protected function sendRegistrationSuccessToast(): void
{ {
$this->toast('Selamat, akun Anda berhasil didaftarkan. Silakan periksa email Anda untuk melakukan verifikasi.'); $this->toast('Selamat, akun Anda berhasil didaftarkan. Silakan periksa email Anda untuk melakukan verifikasi.');
} }
@ -118,7 +119,7 @@ protected function sendRegistrationSuccessToast()
/** /**
* Dispatch notification to privileged users when a new member registers. * Dispatch notification to privileged users when a new member registers.
*/ */
protected function notifyPrivilegedUsersAboutNewMember() protected function notifyPrivilegedUsersAboutNewMember(): void
{ {
$userIds = User::role(['Developer', 'Owner'])->pluck('id')->toArray(); $userIds = User::role(['Developer', 'Owner'])->pluck('id')->toArray();
@ -135,7 +136,7 @@ protected function notifyPrivilegedUsersAboutNewMember()
/** /**
* Redirect user after successful registration. * Redirect user after successful registration.
*/ */
protected function redirectAfterRegistration() protected function redirectAfterRegistration(): void
{ {
$this->redirectIntended('/member/overview', navigate: true); $this->redirectIntended('/member/overview', navigate: true);
} }

View File

@ -5,6 +5,7 @@
use App\Livewire\Forms\Auth\ResetPasswordForm; use App\Livewire\Forms\Auth\ResetPasswordForm;
use App\Traits\WithToast; use App\Traits\WithToast;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Url; use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
@ -19,17 +20,17 @@ class ResetPassword extends Component
public ResetPasswordForm $form; public ResetPasswordForm $form;
#[Url] #[Url]
public $token = ''; public string $token = '';
#[Url] #[Url]
public $email = ''; public string $email = '';
public function mount() public function mount(): void
{ {
$this->form->setTokenAndEmail($this->token, $this->email); $this->form->setTokenAndEmail($this->token, $this->email);
} }
public function resetPassword() public function resetPassword(): mixed
{ {
$status = $this->form->store(); $status = $this->form->store();
@ -40,7 +41,7 @@ public function resetPassword()
: $this->toast(__($status), 'Gagal', 'danger'); : $this->toast(__($status), 'Gagal', 'danger');
} }
public function render() public function render(): View
{ {
return view('livewire.auth.reset-password'); return view('livewire.auth.reset-password');
} }

View File

@ -0,0 +1,24 @@
<?php
namespace App\Livewire\Forms\Auth;
use Livewire\Form;
class ForgotPasswordForm extends Form
{
public string $email = '';
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
];
}
public function validationAttributes(): array
{
return [
'email' => 'email',
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Livewire\Forms\Auth;
use Livewire\Form;
class LoginForm extends Form
{
public string $login = '';
public string $password = '';
public function rules(): array
{
return [
'login' => ['required', 'string'],
'password' => ['required'],
];
}
public function validationAttributes(): array
{
return [
'login' => 'nama pengguna atau email',
'password' => 'kata sandi',
];
}
}

View File

@ -62,17 +62,17 @@ public function rulesStep2(): array
]; ];
} }
public function validateStep1() public function validateStep1(): void
{ {
$this->validate($this->rulesStep1()); $this->validate($this->rulesStep1());
} }
public function validateStep2() public function validateStep2(): void
{ {
$this->validate($this->rulesStep2()); $this->validate($this->rulesStep2());
} }
public function validateAll() public function validateAll(): void
{ {
$this->validate(array_merge( $this->validate(array_merge(
$this->rulesStep1(), $this->rulesStep1(),
@ -91,7 +91,7 @@ public function validationAttributes(): array
]; ];
} }
public function auth() public function auth(): string
{ {
$this->validateAll(); $this->validateAll();
@ -99,7 +99,7 @@ public function auth()
$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) { DB::transaction(function () use ($tier, $maxUser, $referralCode, &$customer): void {
$user = User::create([ $user = User::create([
'email' => $this->email, 'email' => $this->email,
'username' => $this->username, 'username' => $this->username,

View File

@ -18,7 +18,7 @@ class ResetPasswordForm extends Form
public string $password_confirmation = ''; public string $password_confirmation = '';
public function rules() public function rules(): array
{ {
return [ return [
'token' => 'required', 'token' => 'required',
@ -36,13 +36,13 @@ public function validationAttributes(): array
]; ];
} }
public function setTokenAndEmail(string $token, string $email) public function setTokenAndEmail(string $token, string $email): void
{ {
$this->token = $token; $this->token = $token;
$this->email = $email; $this->email = $email;
} }
public function store() public function store(): string
{ {
$this->validate(); $this->validate();

View File

@ -20,9 +20,9 @@ class="size-8 rounded shrink-0 bg-accent text-accent-foreground flex items-cente
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<flux:field> <flux:field>
<flux:label>Email <span class="text-red-500 ms-1">*</span></flux:label> <flux:label>Email <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input wire:model="email" type="text" placeholder="info.pangestuyoga@gmail.com" <flux:input wire:model="form.email" type="text" placeholder="info.pangestuyoga@gmail.com"
autocomplete="off" /> autocomplete="off" />
<flux:error name="email" /> <flux:error name="form.email" />
</flux:field> </flux:field>
<flux:button variant="primary" class="w-full cursor-pointer" wire:click="sendLink">Kirim Tautan <flux:button variant="primary" class="w-full cursor-pointer" wire:click="sendLink">Kirim Tautan

View File

@ -20,9 +20,9 @@ class="size-8 rounded shrink-0 bg-accent text-accent-foreground flex items-cente
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<flux:field> <flux:field>
<flux:label>Nama Pengguna atau Email <span class="text-red-500 ms-1">*</span></flux:label> <flux:label>Nama Pengguna atau Email <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input wire:model="login" type="text" placeholder="johndoe / info.pangestuyoga@gmail.com" <flux:input wire:model="form.login" type="text" placeholder="johndoe / info.pangestuyoga@gmail.com"
autocomplete="off" /> autocomplete="off" />
<flux:error name="login" /> <flux:error name="form.login" />
</flux:field> </flux:field>
<flux:field> <flux:field>
@ -34,9 +34,9 @@ class="size-8 rounded shrink-0 bg-accent text-accent-foreground flex items-cente
sandi?</flux:link> sandi?</flux:link>
</div> </div>
<flux:input wire:model="password" type="password" placeholder="********" viewable /> <flux:input wire:model="form.password" type="password" placeholder="********" viewable />
<flux:error name="password" /> <flux:error name="form.password" />
</flux:field> </flux:field>
<flux:button variant="primary" class="w-full cursor-pointer" wire:click="auth">Masuk</flux:button> <flux:button variant="primary" class="w-full cursor-pointer" wire:click="auth">Masuk</flux:button>

View File

@ -16,7 +16,7 @@
function attemptSendResetLink(string $email) function attemptSendResetLink(string $email)
{ {
return Livewire::test(ForgotPassword::class) return Livewire::test(ForgotPassword::class)
->set('email', $email) ->set('form.email', $email)
->call('sendLink'); ->call('sendLink');
} }
@ -24,7 +24,7 @@ function mockPasswordResetStatus(string $status, string $email = 'user@example.c
{ {
Password::shouldReceive('sendResetLink') Password::shouldReceive('sendResetLink')
->once() ->once()
->with(['email' => $email]) ->with(['form.email' => $email])
->andReturn($status); ->andReturn($status);
} }
@ -37,7 +37,7 @@ function mockPasswordResetStatus(string $status, string $email = 'user@example.c
it('renders forgot password component successfully', function () { it('renders forgot password component successfully', function () {
Livewire::test(ForgotPassword::class) Livewire::test(ForgotPassword::class)
->assertViewIs('livewire.auth.forgot-password') ->assertViewIs('livewire.auth.forgot-password')
->assertSet('email', ''); ->assertSet('form.email', '');
}); });
/* /*
@ -48,9 +48,9 @@ function mockPasswordResetStatus(string $status, string $email = 'user@example.c
it('validates email field is required', function () { it('validates email field is required', function () {
Livewire::test(ForgotPassword::class) Livewire::test(ForgotPassword::class)
->set('email', '') ->set('form.email', '')
->call('sendLink') ->call('sendLink')
->assertHasErrors(['email' => 'required']); ->assertHasErrors(['form.email' => 'required']);
}); });
/* /*

View File

@ -46,8 +46,8 @@ function makeUser(?string $role = null, bool $isActive = true): User
function attemptLogin(string $login, string $password) function attemptLogin(string $login, string $password)
{ {
return Livewire::test(Login::class) return Livewire::test(Login::class)
->set('login', $login) ->set('form.login', $login)
->set('password', $password) ->set('form.password', $password)
->call('auth'); ->call('auth');
} }
@ -60,14 +60,14 @@ function attemptLogin(string $login, string $password)
it('renders login component successfully', function () { it('renders login component successfully', function () {
Livewire::test(Login::class) Livewire::test(Login::class)
->assertViewIs('livewire.auth.login') ->assertViewIs('livewire.auth.login')
->assertSet('login', '') ->assertSet('form.login', '')
->assertSet('password', ''); ->assertSet('form.password', '');
}); });
it('mounts component without query parameters', function () { it('mounts component without query parameters', function () {
Livewire::test(Login::class) Livewire::test(Login::class)
->assertSet('login', '') ->assertSet('form.login', '')
->assertSet('password', ''); ->assertSet('form.password', '');
}); });
it('mounts component with message query parameter', function () { it('mounts component with message query parameter', function () {
@ -75,8 +75,8 @@ function attemptLogin(string $login, string $password)
Livewire::withQueryParams(['message' => $message]) Livewire::withQueryParams(['message' => $message])
->test(Login::class) ->test(Login::class)
->assertSet('login', '') ->assertSet('form.login', '')
->assertSet('password', ''); ->assertSet('form.password', '');
}); });
/* /*
@ -87,26 +87,26 @@ function attemptLogin(string $login, string $password)
it('validates required fields', function () { it('validates required fields', function () {
Livewire::test(Login::class) Livewire::test(Login::class)
->set('login', '') ->set('form.login', '')
->set('password', '') ->set('form.password', '')
->call('auth') ->call('auth')
->assertHasErrors(['login', 'password']); ->assertHasErrors(['form.login', 'form.password']);
}); });
it('validates login field is required', function () { it('validates login field is required', function () {
Livewire::test(Login::class) Livewire::test(Login::class)
->set('login', '') ->set('form.login', '')
->set('password', 'password123') ->set('form.password', 'password123')
->call('auth') ->call('auth')
->assertHasErrors(['login']); ->assertHasErrors(['form.login']);
}); });
it('validates password field is required', function () { it('validates password field is required', function () {
Livewire::test(Login::class) Livewire::test(Login::class)
->set('login', 'testuser') ->set('form.login', 'testuser')
->set('password', '') ->set('form.password', '')
->call('auth') ->call('auth')
->assertHasErrors(['password']); ->assertHasErrors(['form.password']);
}); });
/* /*