From 23cfd0c88d518dfbb3f2bdb3bd448e252eccc3f8 Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Mon, 17 Nov 2025 08:34:07 -0800 Subject: [PATCH] feat: add UserLevel enum and integrate it in User Modal, UserResource and UserFactory --- app/Filament/Resources/UserResource.php | 78 ++++++++++++++++--------- app/Models/User.php | 61 ++++++++++++++++++- app/enum/UserLevel.php | 67 +++++++++++++++++++++ database/factories/UserFactory.php | 34 ++++++++++- database/seeders/UserSeeder.php | 27 +++++++++ 5 files changed, 235 insertions(+), 32 deletions(-) create mode 100644 app/enum/UserLevel.php create mode 100644 database/seeders/UserSeeder.php diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index 7cc1ddd..64143b7 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -2,16 +2,14 @@ namespace App\Filament\Resources; -use BackedEnum; -use UnitEnum; -use Illuminate\Support\Facades\DB; +use App\enum\UserLevel; use App\Filament\Resources\UserResource\Pages\CreateUser; use App\Filament\Resources\UserResource\Pages\EditUser; use App\Filament\Resources\UserResource\Pages\ListUsers; use App\Filament\Resources\UserResource\RelationManagers\LogsRelationManager; use App\Filament\Resources\UserResource\RelationManagers\UsageLogsRelationManager; use App\Models\User; -use Exception; +use BackedEnum; use Filament\Actions\BulkAction; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; @@ -22,12 +20,14 @@ use Filament\Forms\Components\TextInput; use Filament\Notifications\Notification; use Filament\Resources\Resource; use Filament\Schemas\Schema; -use Filament\Tables\Columns\BadgeColumn; use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; +use UnitEnum; class UserResource extends Resource { @@ -75,12 +75,10 @@ class UserResource extends Resource ->displayFormat('Y-m-d H:i:s'), Select::make('level') ->label('User Level') - ->options([ - 0 => 'Normal User', - 1 => 'Banned', - 9 => 'Super Admin', - ]) - ->required(), + ->options(UserLevel::class) + ->enum(UserLevel::class) + ->required() + ->helperText('Select the appropriate user level. Super Admin has full system access.'), ]); } @@ -100,19 +98,12 @@ class UserResource extends Resource ->falseColor('danger') ->getStateUsing(fn ($record): bool => ! is_null($record->email_verified_at)) ->sortable(), - BadgeColumn::make('level') + TextColumn::make('level') ->label('User Level') - ->getStateUsing(fn($record): string => match ($record->level) { - 0 => 'Normal User', - 1 => 'Banned', - 9 => 'Super Admin', - default => 'Unknown', // In case some invalid level exists - }) - ->colors([ - 'success' => fn ($state): bool => $state === 'Normal User', - 'danger' => fn ($state): bool => $state === 'Banned', - 'warning' => fn ($state): bool => $state === 'Super Admin', - ]) + ->badge() + ->getStateUsing(fn ($record): string => $record->level->getLabel()) + ->color(fn ($record): string => $record->level->getColor()) + ->icon(fn ($record): string => $record->level->getIcon()) ->sortable(), TextColumn::make('stripe_id')->label('Stripe ID')->copyable(), TextColumn::make('pm_last_four')->label('Card Last 4'), @@ -120,6 +111,9 @@ class UserResource extends Resource ]) ->defaultSort('created_at', 'desc') ->filters([ + SelectFilter::make('level') + ->label('User Level') + ->options(UserLevel::class), SelectFilter::make('subscription_status') ->label('Subscription Status') ->options([ @@ -162,14 +156,37 @@ class UserResource extends Resource BulkAction::make('updateLevel') ->label('Update User Level') ->action(function (Collection $records, array $data): void { + $newLevel = (int) $data['new_level']; - $newLevel = $data['new_level']; - throw_if($newLevel === 9, Exception::class, 'User level cannot be 9 or higher.'); + // Prevent bulk updating to Super Admin level for security + if ($newLevel === UserLevel::SUPERADMIN->value) { + $message = 'Cannot bulk assign Super Admin level for security reasons.'; + + Log::warning('Attempted bulk Super Admin assignment', [ + 'user_ids' => $records->pluck('id')->toArray(), + 'attempted_level' => $newLevel, + 'ip' => request()->ip(), + ]); + + Notification::make() + ->title('Security Restriction') + ->body($message) + ->danger() + ->send(); + + return; + } DB::table('users') ->whereIn('id', $records->pluck('id')) ->update(['level' => $newLevel]); + Log::info('Bulk user level update completed', [ + 'user_ids' => $records->pluck('id')->toArray(), + 'new_level' => $newLevel, + 'updated_by' => auth()->id(), + ]); + Notification::make() ->title('User Level Updated') ->body('The selected users\' levels have been updated successfully.') @@ -179,15 +196,18 @@ class UserResource extends Resource ->icon('heroicon-o-pencil') ->color('primary') ->modalHeading('Select User Level') - ->modalSubheading('Please choose the user level to apply to the selected users.') + ->modalDescription('Please choose the user level to apply to the selected users.') + ->modalSubmitActionLabel('Update Level') + ->modalCancelActionLabel('Cancel') ->form([ Select::make('new_level') ->label('Select User Level') ->options([ - 0 => 'Unban (Normal User)', - 1 => 'Ban', + UserLevel::NORMALUSER->value => UserLevel::NORMALUSER->getLabel(), + UserLevel::BANNEDUSER->value => UserLevel::BANNEDUSER->getLabel(), ]) - ->required(), + ->required() + ->helperText('Super Admin level cannot be assigned via bulk action for security.'), ]), ]), ]); diff --git a/app/Models/User.php b/app/Models/User.php index ded1dbe..5baecee 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,7 +2,7 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; +use App\enum\UserLevel; use Database\Factories\UserFactory; use Filament\Models\Contracts\FilamentUser; use Filament\Panel; @@ -52,6 +52,7 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'level' => UserLevel::class, ]; } @@ -65,7 +66,63 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail public function canAccessPanel(Panel $panel): bool { - return str_ends_with($this->email, '@zemail.me') && $this->level === 9 && $this->hasVerifiedEmail(); + return str_ends_with($this->email, '@zemail.me') && $this->level === UserLevel::SUPERADMIN && $this->hasVerifiedEmail(); + } + + /** + * Scope to query only super admin users. + */ + public function scopeIsSuperAdmin($query) + { + return $query->where('level', UserLevel::SUPERADMIN->value); + } + + /** + * Scope to query only normal users. + */ + public function scopeIsNormalUser($query) + { + return $query->where('level', UserLevel::NORMALUSER->value); + } + + /** + * Scope to query only banner users. + */ + public function scopeIsBannerUser($query) + { + return $query->where('level', UserLevel::BANNEDUSER->value); + } + + /** + * Check if user is a super admin. + */ + public function isSuperAdmin(): bool + { + return $this->level === UserLevel::SUPERADMIN; + } + + /** + * Check if user is a normal user. + */ + public function isNormalUser(): bool + { + return $this->level === UserLevel::NORMALUSER; + } + + /** + * Check if user is a banner user. + */ + public function isBannerUser(): bool + { + return $this->level === UserLevel::BANNEDUSER; + } + + /** + * Get user level name. + */ + public function getLevelName(): string + { + return $this->level->name; } public function tickets(): HasMany diff --git a/app/enum/UserLevel.php b/app/enum/UserLevel.php new file mode 100644 index 0000000..546f3e3 --- /dev/null +++ b/app/enum/UserLevel.php @@ -0,0 +1,67 @@ + 'Super Admin', + self::NORMALUSER => 'Normal User', + self::BANNEDUSER => 'Banned User', + }; + } + + /** + * Get the color for the enum value (for UI display). + */ + public function getColor(): string + { + return match ($this) { + self::SUPERADMIN => 'danger', // Red for highest privilege + self::NORMALUSER => 'success', // Green for regular users + self::BANNEDUSER => 'warning', // Yellow/Orange for banned users + }; + } + + /** + * Get the icon for the enum value. + */ + public function getIcon(): string + { + return match ($this) { + self::SUPERADMIN => 'heroicon-o-shield-check', + self::NORMALUSER => 'heroicon-o-user', + self::BANNEDUSER => 'heroicon-o-no-symbol', + }; + } + + /** + * Get all options as array for select fields. + */ + public static function getSelectOptions(): array + { + return collect(self::cases())->mapWithKeys(fn ($case) => [ + $case->value => $case->getLabel(), + ])->toArray(); + } + + /** + * Get the badge configuration for Filament. + */ + public function getBadge(): array + { + return [ + 'name' => $this->getLabel(), + 'color' => $this->getColor(), + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 4a2485e..42eeaa3 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\enum\UserLevel; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; @@ -30,7 +31,7 @@ class UserFactory extends Factory 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), - 'level' => 0, + 'level' => UserLevel::NORMALUSER->value, ]; } @@ -43,4 +44,35 @@ class UserFactory extends Factory 'email_verified_at' => null, ]); } + + /** + * Create a super admin user. + */ + public function superAdmin(): static + { + return $this->state(fn (array $attributes): array => [ + 'level' => UserLevel::SUPERADMIN->value, + 'email' => 'admin@zemail.me', + ]); + } + + /** + * Create a normal user. + */ + public function normalUser(): static + { + return $this->state(fn (array $attributes): array => [ + 'level' => UserLevel::NORMALUSER->value, + ]); + } + + /** + * Create a banned user. + */ + public function bannedUser(): static + { + return $this->state(fn (array $attributes): array => [ + 'level' => UserLevel::BANNEDUSER->value, + ]); + } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php new file mode 100644 index 0000000..ff680e7 --- /dev/null +++ b/database/seeders/UserSeeder.php @@ -0,0 +1,27 @@ +superAdmin()->create([ + 'name' => 'Super Admin', + 'email' => 'admin@zemail.me', + ]); + + // Create normal users + User::factory()->normalUser()->count(5)->create(); + + // Create banned users + User::factory()->bannedUser()->count(2)->create(); + } +}