feat: Implement a custom Filament login page supporting username or email and integrate cheerful notifications for user feedback.
This commit is contained in:
parent
c5fa78840a
commit
a2c5c94cb8
151
app/Filament/Pages/Auth/Login.php
Normal file
151
app/Filament/Pages/Auth/Login.php
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages\Auth;
|
||||||
|
|
||||||
|
use App\Filament\Support\CheerfulNotification;
|
||||||
|
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
|
||||||
|
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
|
||||||
|
use Filament\Auth\MultiFactor\Contracts\HasBeforeChallengeHook;
|
||||||
|
use Filament\Auth\Pages\Login as FilamentLogin;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Illuminate\Auth\SessionGuard;
|
||||||
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
use Illuminate\Contracts\Support\Htmlable;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Support\HtmlString;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use SensitiveParameter;
|
||||||
|
|
||||||
|
class Login extends FilamentLogin
|
||||||
|
{
|
||||||
|
public function getHeading(): string|Htmlable
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
TextInput::make('login')
|
||||||
|
->label('Nama Pengguna atau Alamat Surel')
|
||||||
|
->placeholder('johndoe@example.com')
|
||||||
|
->required()
|
||||||
|
->autocomplete(false)
|
||||||
|
->autofocus(),
|
||||||
|
|
||||||
|
TextInput::make('password')
|
||||||
|
->label('Kata Sandi')
|
||||||
|
->placeholder('********')
|
||||||
|
->password()
|
||||||
|
->revealable(filament()->arePasswordsRevealable())
|
||||||
|
->required()
|
||||||
|
->autocomplete(false)
|
||||||
|
->extraInputAttributes(['tabindex' => 2])
|
||||||
|
->hint(
|
||||||
|
filament()->hasPasswordReset()
|
||||||
|
? new HtmlString(Blade::render('<x-filament::link :href="filament()->getRequestPasswordResetUrl()" tabindex="3">{{ __(\'filament-panels::auth/pages/login.actions.request_password_reset.label\') }}</x-filament::link>'))
|
||||||
|
: null
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getCredentialsFromFormData(#[SensitiveParameter] array $data): array
|
||||||
|
{
|
||||||
|
$loginType = filter_var($data['login'], FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
|
||||||
|
|
||||||
|
return [
|
||||||
|
$loginType => $data['login'],
|
||||||
|
'password' => $data['password'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(): ?LoginResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->rateLimit(5);
|
||||||
|
} catch (TooManyRequestsException $exception) {
|
||||||
|
$this->getRateLimitedNotification($exception)?->send();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->form->getState();
|
||||||
|
|
||||||
|
/** @var SessionGuard $authGuard */
|
||||||
|
$authGuard = Filament::auth();
|
||||||
|
|
||||||
|
$authProvider = $authGuard->getProvider();
|
||||||
|
/** @phpstan-ignore-line */
|
||||||
|
$credentials = $this->getCredentialsFromFormData($data);
|
||||||
|
|
||||||
|
$user = $authProvider->retrieveByCredentials($credentials);
|
||||||
|
|
||||||
|
if ((! $user) || (! $authProvider->validateCredentials($user, $credentials))) {
|
||||||
|
$this->userUndertakingMultiFactorAuthentication = null;
|
||||||
|
|
||||||
|
$this->fireFailedEvent($authGuard, $user, $credentials);
|
||||||
|
$this->throwFailureValidationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filled($this->userUndertakingMultiFactorAuthentication) &&
|
||||||
|
(decrypt($this->userUndertakingMultiFactorAuthentication) === $user->getAuthIdentifier())
|
||||||
|
) {
|
||||||
|
$this->multiFactorChallengeForm->validate();
|
||||||
|
} else {
|
||||||
|
foreach (Filament::getMultiFactorAuthenticationProviders() as $multiFactorAuthenticationProvider) {
|
||||||
|
if (! $multiFactorAuthenticationProvider->isEnabled($user)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->userUndertakingMultiFactorAuthentication = encrypt($user->getAuthIdentifier());
|
||||||
|
|
||||||
|
if ($multiFactorAuthenticationProvider instanceof HasBeforeChallengeHook) {
|
||||||
|
$multiFactorAuthenticationProvider->beforeChallenge($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filled($this->userUndertakingMultiFactorAuthentication)) {
|
||||||
|
$this->multiFactorChallengeForm->fill();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $authGuard->attemptWhen($credentials, function (Authenticatable $user): bool {
|
||||||
|
if (! ($user instanceof FilamentUser)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user->canAccessPanel(Filament::getCurrentOrDefaultPanel());
|
||||||
|
}, $data['remember'] ?? false)) {
|
||||||
|
$this->fireFailedEvent($authGuard, $user, $credentials);
|
||||||
|
$this->throwFailureValidationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->regenerate();
|
||||||
|
|
||||||
|
CheerfulNotification::success(
|
||||||
|
'Hore! Berhasil Masuk 🎉',
|
||||||
|
'Selamat datang kembali, '.$user->name.'! Siap untuk beraksi hari ini? 🚀✨'
|
||||||
|
)->send();
|
||||||
|
|
||||||
|
return app(LoginResponse::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function throwFailureValidationException(): never
|
||||||
|
{
|
||||||
|
CheerfulNotification::danger(
|
||||||
|
'Ups! Gagal Masuk 😅',
|
||||||
|
'Sepertinya ada yang salah nih. Coba cek email atau kata sandi mu lagi ya! 🤔🔐'
|
||||||
|
)->send();
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
159
app/Filament/Support/CheerfulNotification.php
Normal file
159
app/Filament/Support/CheerfulNotification.php
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Support;
|
||||||
|
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
|
||||||
|
class CheerfulNotification
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*/
|
||||||
|
public static function make(): Notification
|
||||||
|
{
|
||||||
|
return Notification::make();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification for successful record creation.
|
||||||
|
*/
|
||||||
|
public static function create(): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title('Hore! Data Tersimpan 🎉✨')
|
||||||
|
->body('Data baru berhasil ditambahkan! Sistem sudah menyimpannya dengan aman. 🚀💪')
|
||||||
|
->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification for successful record update.
|
||||||
|
*/
|
||||||
|
public static function update(): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title('Mantap! Data Diperbarui ✅🔥')
|
||||||
|
->body('Perubahan berhasil disimpan! Data sekarang sudah up-to-date dan segar lagi. ✨👌')
|
||||||
|
->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification for successful record deletion (soft delete).
|
||||||
|
*/
|
||||||
|
public static function delete(): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title('Oke, Data Dihapus 🗑️👋')
|
||||||
|
->body('Data tersebut sudah berhasil dihapus dari sistem. Semuanya bersih dan rapi sekarang! 😊🚮')
|
||||||
|
->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification for successful permanent deletion.
|
||||||
|
*/
|
||||||
|
public static function forceDelete(): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title('Selamat Tinggal Selamanya 👋😢')
|
||||||
|
->body('Data telah dihapus permanen dan tidak bisa kembali. Semoga ini keputusan yang tepat! 🚮💨')
|
||||||
|
->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification for successful record restoration.
|
||||||
|
*/
|
||||||
|
public static function restore(): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title('Welcome Back! Data Pulih ♻️✨')
|
||||||
|
->body('Data berhasil dikembalikan! Hati-hati ya, jangan sampai terhapus lagi. 😉👍')
|
||||||
|
->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification for successful status change/toggle.
|
||||||
|
*/
|
||||||
|
public static function statusUpdated(?string $title = null, ?string $body = null): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title($title ?? 'Status Berubah! 🔄✨')
|
||||||
|
->body($body ?? 'Status data berhasil diperbarui. Perubahan langsung aktif ya! 👍')
|
||||||
|
->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification for successful bulk deletion.
|
||||||
|
*/
|
||||||
|
public static function bulkDelete(): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title('Oke, Banyak Data Dihapus 🗑️👋')
|
||||||
|
->body('Semua data yang dipilih berhasil dihapus. Sistem makin lega deh! 😊')
|
||||||
|
->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification for successful bulk permanent deletion.
|
||||||
|
*/
|
||||||
|
public static function bulkForceDelete(): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title('Bye Bye Semua! 👋🔥')
|
||||||
|
->body('Data yang dipilih sudah dihapus permanen. Bersih total! 🧹💨')
|
||||||
|
->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification for successful bulk restoration.
|
||||||
|
*/
|
||||||
|
public static function bulkRestore(): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title('Hore! Banyak Data Pulih ♻️🎉')
|
||||||
|
->body('Data-data tersebut sudah kembali aktif. Selamat bekerja kembali! 💪✨')
|
||||||
|
->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom cheerful success notification.
|
||||||
|
*/
|
||||||
|
public static function success(string $title, string $body): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title($title)
|
||||||
|
->body($body)
|
||||||
|
->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom cheerful info notification.
|
||||||
|
*/
|
||||||
|
public static function info(string $title, string $body): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title($title)
|
||||||
|
->body($body)
|
||||||
|
->info();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom cheerful warning notification.
|
||||||
|
*/
|
||||||
|
public static function warning(string $title, string $body): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title($title)
|
||||||
|
->body($body)
|
||||||
|
->warning();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom cheerful danger notification.
|
||||||
|
*/
|
||||||
|
public static function danger(string $title, string $body): Notification
|
||||||
|
{
|
||||||
|
return self::make()
|
||||||
|
->title($title)
|
||||||
|
->body($body)
|
||||||
|
->danger();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers\Filament;
|
namespace App\Providers\Filament;
|
||||||
|
|
||||||
|
use App\Filament\Pages\Auth\Login;
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
use Filament\Http\Middleware\AuthenticateSession;
|
use Filament\Http\Middleware\AuthenticateSession;
|
||||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||||
@ -27,7 +28,7 @@ public function panel(Panel $panel): Panel
|
|||||||
->default()
|
->default()
|
||||||
->id('admin')
|
->id('admin')
|
||||||
->path('admin')
|
->path('admin')
|
||||||
->login()
|
->login(Login::class)
|
||||||
->colors([
|
->colors([
|
||||||
'primary' => Color::Amber,
|
'primary' => Color::Amber,
|
||||||
])
|
])
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user