diff --git a/app/Filament/Resources/Users/Pages/CreateUser.php b/app/Filament/Resources/Users/Pages/CreateUser.php index 125b3ff..ea8ef05 100644 --- a/app/Filament/Resources/Users/Pages/CreateUser.php +++ b/app/Filament/Resources/Users/Pages/CreateUser.php @@ -4,8 +4,16 @@ use App\Filament\Resources\Users\UserResource; use Filament\Resources\Pages\CreateRecord; +use Illuminate\Support\Facades\Hash; class CreateUser extends CreateRecord { protected static string $resource = UserResource::class; + + protected function mutateFormDataBeforeCreate(array $data): array + { + $data['password'] = Hash::make($data['password']); + + return $data; + } } diff --git a/app/Filament/Resources/Users/Pages/EditUser.php b/app/Filament/Resources/Users/Pages/EditUser.php index 4c7c132..6948a85 100644 --- a/app/Filament/Resources/Users/Pages/EditUser.php +++ b/app/Filament/Resources/Users/Pages/EditUser.php @@ -5,6 +5,7 @@ use App\Filament\Resources\Users\UserResource; use Filament\Actions\DeleteAction; use Filament\Resources\Pages\EditRecord; +use Illuminate\Support\Facades\Hash; class EditUser extends EditRecord { @@ -16,4 +17,15 @@ protected function getHeaderActions(): array DeleteAction::make(), ]; } + + protected function mutateFormDataBeforeSave(array $data): array + { + $password = $data['password'] ?? null; + + if ($password) { + $data['password'] = Hash::make($password); + } + + return $data; + } } diff --git a/app/Filament/Resources/Users/Schemas/UserForm.php b/app/Filament/Resources/Users/Schemas/UserForm.php index 2bb394a..8830d41 100644 --- a/app/Filament/Resources/Users/Schemas/UserForm.php +++ b/app/Filament/Resources/Users/Schemas/UserForm.php @@ -5,43 +5,50 @@ use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; class UserForm { public static function configure(Schema $schema): Schema { - return $schema + return $schema->columns(6) ->components([ - TextInput::make('first_name') - ->autofocus() - ->required() - ->maxLength(255), - - TextInput::make('last_name') - ->required() - ->maxLength(255), - - TextInput::make('email') - ->required() - ->email() - ->maxLength(255), - - TextInput::make('password') - ->required(fn (string $operation): bool => $operation === 'create') - ->password() - ->afterStateHydrated(function (TextInput $component, $state) { - $component->state(''); - }) - ->dehydrated(fn (?string $state): bool => filled($state)) - ->maxLength(255), - - DateTimePicker::make('email_verified_at'), - - Select::make('roles') - ->preload() - ->multiple() - ->relationship('roles', 'name'), + + Section::make('Settings') + ->columnSpanFull() + ->columns(2) + ->schema([ + TextInput::make('first_name') + ->autofocus() + ->required() + ->maxLength(255), + + TextInput::make('last_name') + ->required() + ->maxLength(255), + + TextInput::make('email') + ->required() + ->email() + ->maxLength(255), + + TextInput::make('password') + ->required(fn (string $operation): bool => $operation === 'create') + ->password() + ->afterStateHydrated(function (TextInput $component, $state) { + $component->state(''); + }) + ->dehydrated(fn (?string $state): bool => filled($state)) + ->maxLength(255), + + DateTimePicker::make('email_verified_at'), + + Select::make('roles') + ->preload() + ->multiple() + ->relationship('roles', 'name'), + ]), ]); } } diff --git a/app/Filament/Resources/Users/Tables/UsersTable.php b/app/Filament/Resources/Users/Tables/UsersTable.php index b1c7636..0d69541 100644 --- a/app/Filament/Resources/Users/Tables/UsersTable.php +++ b/app/Filament/Resources/Users/Tables/UsersTable.php @@ -2,11 +2,15 @@ namespace App\Filament\Resources\Users\Tables; +use App\Enums\Role; +use App\Models\User; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; -use Filament\Tables\Columns\IconColumn; +use Filament\Support\Icons\Heroicon; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Filters\SelectFilter; +use Filament\Tables\Filters\TernaryFilter; use Filament\Tables\Table; class UsersTable @@ -21,28 +25,46 @@ public static function configure(Table $table): Table ->sortable(), TextColumn::make('email') + ->tooltip(fn (User $user) => $user->hasVerifiedEmail() ? 'Email Verified' : 'Email Not Verified') + ->icon(fn (User $user) => match ($user->hasVerifiedEmail()) { + true => Heroicon::OutlinedCheckCircle, + false => Heroicon::OutlinedXCircle, + }) + ->iconColor(fn (User $user) => match ($user->hasVerifiedEmail()) { + true => 'success', + false => 'danger', + }) ->searchable() ->sortable(), - IconColumn::make('email_verified') - ->getStateUsing(fn ($record) => $record->email_verified_at) - ->boolean() - ->sortable(), - TextColumn::make('roles') - ->getStateUsing(fn ($record) => $record->roles->pluck('name')->join(', ')) + ->badge() + ->getStateUsing(fn ($record) => $record->roles->pluck('name')) ->searchable() ->sortable(), TextColumn::make('created_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), + ->toggleable(isToggledHiddenByDefault: true) + ->sortable(), TextColumn::make('updated_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), + ->toggleable(isToggledHiddenByDefault: true) + ->sortable(), + ]) + ->filters([ + TernaryFilter::make('email_verified_at') + ->label('Email Verification') + ->nullable() + ->placeholder('All') + ->trueLabel('Verified') + ->falseLabel('Unverified'), + + SelectFilter::make('roles') + ->label('Roles') + ->relationship('roles', 'name') + ->options(Role::values()) + ->multiple() + ->preload(), ]) ->recordActions([ EditAction::make(), diff --git a/app/Http/Controllers/RegisterController.php b/app/Http/Controllers/RegisterController.php index b28d6b0..ce788d8 100644 --- a/app/Http/Controllers/RegisterController.php +++ b/app/Http/Controllers/RegisterController.php @@ -7,6 +7,7 @@ use App\Http\Requests\Register\RegisterStoreRequest; use App\Models\User; use Filament\Auth\Events\Registered; +use Illuminate\Support\Facades\Hash; class RegisterController extends Controller { @@ -24,7 +25,7 @@ public function store(RegisterStoreRequest $request) { $user = new User($request->only('first_name', 'last_name', 'email')); - $user->password = $request->validated('password'); + $user->password = Hash::make($request->validated('password')); $user->save(); $user->assignRole(Role::USER->value); diff --git a/app/Http/Controllers/ResetPasswordController.php b/app/Http/Controllers/ResetPasswordController.php index 16d7cbc..3fab382 100644 --- a/app/Http/Controllers/ResetPasswordController.php +++ b/app/Http/Controllers/ResetPasswordController.php @@ -7,6 +7,7 @@ use App\Models\User; use Illuminate\Auth\Events\PasswordReset; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Password; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; @@ -43,9 +44,9 @@ public function edit(Request $request, string $token) public function update(ResetPasswordUpdateRequest $request) { - $status = Password::reset($request->only('token', 'email', 'password', 'token'), function (User $user, string $password) { + $status = Password::reset($request->only('token', 'email', 'password', 'password_confirmation'), function (User $user, string $password) { $user->forceFill([ - 'password' => $password, + 'password' => Hash::make($password), ])->setRememberToken(Str::random(60)); $user->save(); diff --git a/app/Models/User.php b/app/Models/User.php index 352bbef..598a746 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -42,13 +42,6 @@ protected function casts(): array ]; } - protected function password(): Attribute - { - return Attribute::make( - set: fn ($value) => Hash::make($value), - ); - } - protected function fullName(): Attribute { return Attribute::make( @@ -71,7 +64,7 @@ protected function scopeHasRoles(Builder $query, array $roles): void public function updatePassword(?string $new_password = '') { if ($new_password && $new_password != $this->password) { - $this->password = $new_password; + $this->password = Hash::make($new_password); $this->save(); } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 55d85b2..9219e69 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -25,10 +25,12 @@ public function panel(Panel $panel): Panel ->default() ->id('admin') ->path('admin') - ->login(Login::class) + ->topbar(false) + ->viteTheme('resources/css/filament/admin/theme.css') ->colors([ 'primary' => '#1e293b', ]) + ->login(Login::class) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->pages([ diff --git a/resources/css/filament/admin/theme.css b/resources/css/filament/admin/theme.css new file mode 100644 index 0000000..506739a --- /dev/null +++ b/resources/css/filament/admin/theme.css @@ -0,0 +1,4 @@ +@import '../../../../vendor/filament/filament/resources/css/theme.css'; + +@source '../../../../app/Filament/**/*'; +@source '../../../../resources/views/filament/**/*'; diff --git a/tests/Feature/Controllers/LoginControllerTest.php b/tests/Feature/Controllers/LoginControllerTest.php index 280986e..00476a1 100644 --- a/tests/Feature/Controllers/LoginControllerTest.php +++ b/tests/Feature/Controllers/LoginControllerTest.php @@ -2,6 +2,7 @@ use App\Enums\Environment; use App\Models\User; +use Illuminate\Support\Facades\Hash; use Inertia\Testing\AssertableInertia as Assert; use function Pest\Faker\fake; @@ -54,7 +55,7 @@ test('Can login', function () { $password = fake()->password(); $user = User::factory()->create([ - 'password' => $password, + 'password' => Hash::make($password), ]); assertGuest(); @@ -72,7 +73,7 @@ test('Can be redirected after login', function () { $password = fake()->password(); $user = User::factory()->create([ - 'password' => $password, + 'password' => Hash::make($password), ]); $redirectUrl = 'https://www.google.com/'; @@ -93,7 +94,7 @@ test("Can't login with invalid credentials", function () { $user = User::factory()->create([ - 'password' => fake()->password(), + 'password' => Hash::make(fake()->password()), ]); assertGuest(); diff --git a/tests/Feature/Controllers/ResetPasswordControllerTest.php b/tests/Feature/Controllers/ResetPasswordControllerTest.php index 175f8a6..7f24ba4 100644 --- a/tests/Feature/Controllers/ResetPasswordControllerTest.php +++ b/tests/Feature/Controllers/ResetPasswordControllerTest.php @@ -69,7 +69,7 @@ test('Users can reset their passwords', function () { $user = User::factory()->create([ - 'password' => fake()->password(6), + 'password' => Hash::make(fake()->password(6)), ]); $token = Password::createToken($user); diff --git a/tests/Integration/UserTest.php b/tests/Integration/UserTest.php index ee3ae6d..86b4bff 100644 --- a/tests/Integration/UserTest.php +++ b/tests/Integration/UserTest.php @@ -15,7 +15,7 @@ it("can update it's password", function () { $user = User::factory()->create([ - 'password' => 'oldPassword#123', + 'password' => Hash::make('oldPassword#123'), ]); expect(Hash::check('oldPassword#123', $user->password))->toBeTrue(); @@ -27,7 +27,7 @@ it("doesn't update it's password if the value is empty", function () { $user = User::factory()->create([ - 'password' => 'oldPassword#123', + 'password' => Hash::make('oldPassword#123'), ]); expect(Hash::check('oldPassword#123', $user->password))->toBeTrue(); diff --git a/vite.config.js b/vite.config.js index 6c62d4c..eec64d8 100644 --- a/vite.config.js +++ b/vite.config.js @@ -10,6 +10,7 @@ export default defineConfig({ input: [ 'resources/css/app.css', 'resources/js/app.js', + 'resources/css/filament/admin/theme.css', ], refresh: true, }), @@ -37,21 +38,4 @@ export default defineConfig({ '@css': '/resources/css', }, }, - - build: { - sourcemap: false, - rollupOptions: { - output: { - manualChunks: (id) => { - if (id.includes('node_modules')) { - return 'vendor'; - } - - if (id.includes('resources/js')) { - return 'app'; - } - }, - }, - }, - }, });