diff --git a/app/Console/Commands/UpdateUserRole.php b/app/Console/Commands/UpdateUserRole.php new file mode 100644 index 000000000..a3c4072d1 --- /dev/null +++ b/app/Console/Commands/UpdateUserRole.php @@ -0,0 +1,67 @@ + match (true) { + ! User::firstWhere('email', $value) => 'User not found.', + default => null + } + ); + + $role = select( + label: 'What role should the user have?', + options: [ + 'admin' => 'Admin', + 'guest' => 'Guest', + 'user' => 'User', + ], + default: 'guest' + ); + + $confirmed = confirm( + label: 'Are you sure?', + required: true + ); + + if ($confirmed) { + User::firstWhere('email', $email) + ->update([ + 'role' => $role, + ]); + + info('User role updated.'); + } + } +} diff --git a/app/Filament/Pages/Dashboard.php b/app/Filament/Pages/Dashboard.php index 5910c8f83..c8174d51d 100644 --- a/app/Filament/Pages/Dashboard.php +++ b/app/Filament/Pages/Dashboard.php @@ -18,17 +18,13 @@ class Dashboard extends BasePage protected static string $view = 'filament.pages.dashboard'; - protected function getPollingInterval(): ?string - { - return null; - } - protected function getHeaderActions(): array { return [ Action::make('speedtest') ->label('Queue Speedtest') - ->action('queueSpeedtest'), + ->action('queueSpeedtest') + ->hidden(fn (): bool => ! auth()->user()->is_admin && ! auth()->user()->is_user), ]; } diff --git a/app/Filament/Pages/DeleteData.php b/app/Filament/Pages/DeleteData.php index 5c9b761f6..df6f2c3c2 100644 --- a/app/Filament/Pages/DeleteData.php +++ b/app/Filament/Pages/DeleteData.php @@ -21,6 +21,16 @@ class DeleteData extends Page protected ?string $maxContentWidth = '3xl'; + public function mount(): void + { + abort_unless(auth()->user()->is_admin, 403); + } + + public static function shouldRegisterNavigation(): bool + { + return auth()->user()->is_admin; + } + public function getHeaderActions(): array { return [ diff --git a/app/Filament/Pages/Settings/GeneralPage.php b/app/Filament/Pages/Settings/GeneralPage.php index 039407ac5..6dbce6f04 100644 --- a/app/Filament/Pages/Settings/GeneralPage.php +++ b/app/Filament/Pages/Settings/GeneralPage.php @@ -25,6 +25,16 @@ class GeneralPage extends SettingsPage protected static string $settings = GeneralSettings::class; + public function mount(): void + { + abort_unless(auth()->user()->is_admin, 403); + } + + public static function shouldRegisterNavigation(): bool + { + return auth()->user()->is_admin; + } + public function form(Form $form): Form { return $form diff --git a/app/Filament/Pages/Settings/InfluxDbPage.php b/app/Filament/Pages/Settings/InfluxDbPage.php index 5f306a31c..6c448f3b4 100644 --- a/app/Filament/Pages/Settings/InfluxDbPage.php +++ b/app/Filament/Pages/Settings/InfluxDbPage.php @@ -21,6 +21,16 @@ class InfluxDbPage extends SettingsPage protected static string $settings = InfluxDbSettings::class; + public function mount(): void + { + abort_unless(auth()->user()->is_admin, 403); + } + + public static function shouldRegisterNavigation(): bool + { + return auth()->user()->is_admin; + } + public function form(Form $form): Form { return $form diff --git a/app/Filament/Pages/Settings/NotificationPage.php b/app/Filament/Pages/Settings/NotificationPage.php index 3f089db6a..7682dc046 100755 --- a/app/Filament/Pages/Settings/NotificationPage.php +++ b/app/Filament/Pages/Settings/NotificationPage.php @@ -29,6 +29,16 @@ class NotificationPage extends SettingsPage protected static string $settings = NotificationSettings::class; + public function mount(): void + { + abort_unless(auth()->user()->is_admin, 403); + } + + public static function shouldRegisterNavigation(): bool + { + return auth()->user()->is_admin; + } + public function form(Form $form): Form { return $form diff --git a/app/Filament/Pages/Settings/ThresholdsPage.php b/app/Filament/Pages/Settings/ThresholdsPage.php index cb51d6b5a..d27bf0cd5 100644 --- a/app/Filament/Pages/Settings/ThresholdsPage.php +++ b/app/Filament/Pages/Settings/ThresholdsPage.php @@ -21,6 +21,16 @@ class ThresholdsPage extends SettingsPage protected static string $settings = ThresholdSettings::class; + public function mount(): void + { + abort_unless(auth()->user()->is_admin, 403); + } + + public static function shouldRegisterNavigation(): bool + { + return auth()->user()->is_admin; + } + public function form(Form $form): Form { return $form diff --git a/app/Filament/Resources/ResultResource.php b/app/Filament/Resources/ResultResource.php index 70fb80868..e9f649d75 100644 --- a/app/Filament/Resources/ResultResource.php +++ b/app/Filament/Resources/ResultResource.php @@ -26,8 +26,6 @@ class ResultResource extends Resource protected static ?string $navigationIcon = 'heroicon-o-table-cells'; - protected static ?string $navigationLabel = 'Results'; - public static function form(Form $form): Form { $settings = new GeneralSettings(); @@ -168,6 +166,7 @@ public static function table(Table $table): Table Tables\Actions\ViewAction::make(), Tables\Actions\Action::make('updateComments') ->icon('heroicon-o-chat-bubble-bottom-center-text') + ->hidden(fn (): bool => ! auth()->user()->is_admin && ! auth()->user()->is_user) ->mountUsing(fn (Forms\ComponentContainer $form, Result $record) => $form->fill([ 'comments' => $record->comments, ])) @@ -188,6 +187,7 @@ public static function table(Table $table): Table Tables\Actions\BulkAction::make('export') ->label('Export selected') ->icon('heroicon-o-arrow-down-tray') + ->hidden(fn (): bool => ! auth()->user()->is_admin) ->action(function (Collection $records) { $export = new ResultsSelectedBulkExport($records->toArray()); diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index 5282bf38b..eca4ae45d 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -19,13 +19,7 @@ class UserResource extends Resource { protected static ?string $model = User::class; - protected static ?string $navigationGroup = 'System'; - - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; - - protected static ?int $navigationSort = 0; - - protected static ?string $slug = 'system/users'; + protected static ?string $navigationIcon = 'heroicon-o-users'; public static function form(Form $form): Form { @@ -65,20 +59,48 @@ public static function form(Form $form): Form ->visible(fn ($livewire) => $livewire instanceof EditUser) ->dehydrated(false), ]) - ->columns('full') + ->columns(1) ->columnSpan([ 'md' => 2, ]), - Forms\Components\Section::make() + Forms\Components\Grid::make([ + 'default' => 1, + ]) ->schema([ - Forms\Components\Placeholder::make('created_at') - ->content(fn ($record) => $record?->created_at?->diffForHumans() ?? new HtmlString('—')), - Forms\Components\Placeholder::make('updated_at') - ->content(fn ($record) => $record?->updated_at?->diffForHumans() ?? new HtmlString('—')), + Forms\Components\Section::make() + ->schema([ + Forms\Components\Select::make('role') + ->options([ + 'admin' => 'Admin', + 'guest' => 'Guest', + 'user' => 'User', + ]) + ->default('guest') + ->disabled(fn (): bool => ! auth()->user()->is_admin || auth()->user()->is_user) + ->required(), + ]) + ->columns(1) + ->columnSpan([ + 'md' => 1, + ]), + + Forms\Components\Section::make() + ->schema([ + Forms\Components\Placeholder::make('created_at') + ->content(fn ($record) => $record?->created_at?->diffForHumans() ?? new HtmlString('—')), + Forms\Components\Placeholder::make('updated_at') + ->content(fn ($record) => $record?->updated_at?->diffForHumans() ?? new HtmlString('—')), + ]) + ->columns(1) + ->columnSpan([ + 'md' => 1, + ]), ]) - ->columns('full') - ->columnSpan(1), + ->columns(1) + ->columnSpan([ + 'md' => 1, + ]), ]), ]); } @@ -87,27 +109,36 @@ public static function table(Table $table): Table { return $table ->columns([ + Tables\Columns\TextColumn::make('id') + ->label('ID'), Tables\Columns\TextColumn::make('name') ->searchable(), Tables\Columns\TextColumn::make('email') ->searchable(), - Tables\Columns\TextColumn::make('email_verified_at') - ->dateTime(), - Tables\Columns\TextColumn::make('created_at') - ->dateTime(), + Tables\Columns\TextColumn::make('role') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'admin' => 'success', + 'guest' => 'gray', + 'user' => 'info', + }), Tables\Columns\TextColumn::make('updated_at') + ->label('Last updated') ->dateTime(), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('role') + ->options([ + 'admin' => 'Admin', + 'guest' => 'Guest', + 'user' => 'User', + ]), ]) ->actions([ - Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make() - ->requiresConfirmation(), + Tables\Actions\ActionGroup::make([ + Tables\Actions\ViewAction::make(), + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), ]), ]); } diff --git a/app/Models/User.php b/app/Models/User.php index f75e81e1f..b3a68d227 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,6 +6,7 @@ use Filament\Models\Contracts\FilamentUser; use Filament\Panel; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -25,6 +26,7 @@ class User extends Authenticatable implements FilamentUser 'email', 'email_verified_at', 'password', + 'role', ]; /** @@ -53,4 +55,34 @@ public function canAccessPanel(Panel $panel): bool { return true; } + + /** + * Determine if the user has an admin role. + */ + protected function isAdmin(): Attribute + { + return Attribute::make( + get: fn (mixed $value, array $attributes): bool => $attributes['role'] == 'admin', + ); + } + + /** + * Determine if the user has a guest role. + */ + protected function isGuest(): Attribute + { + return Attribute::make( + get: fn (mixed $value, array $attributes): bool => $attributes['role'] == 'guest' || blank($attributes['role']), + ); + } + + /** + * Determine if the user has a user role. + */ + protected function isUser(): Attribute + { + return Attribute::make( + get: fn (mixed $value, array $attributes): bool => $attributes['role'] == 'user', + ); + } } diff --git a/app/Policies/ResultPolicy.php b/app/Policies/ResultPolicy.php index 1a6d46537..cfb3e943f 100644 --- a/app/Policies/ResultPolicy.php +++ b/app/Policies/ResultPolicy.php @@ -4,16 +4,11 @@ use App\Models\Result; use App\Models\User; -use Illuminate\Auth\Access\HandlesAuthorization; class ResultPolicy { - use HandlesAuthorization; - /** * Determine whether the user can view any models. - * - * @return \Illuminate\Auth\Access\Response|bool */ public function viewAny(User $user): bool { @@ -22,8 +17,6 @@ public function viewAny(User $user): bool /** * Determine whether the user can view the model. - * - * @return \Illuminate\Auth\Access\Response|bool */ public function view(User $user, Result $result): bool { @@ -32,61 +25,50 @@ public function view(User $user, Result $result): bool /** * Determine whether the user can create models. - * - * @return \Illuminate\Auth\Access\Response|bool */ public function create(User $user): bool { - // + return false; } /** * Determine whether the user can update the model. - * - * @return \Illuminate\Auth\Access\Response|bool */ public function update(User $user, Result $result): bool { - return true; + return $user->is_admin + || $user->is_user; } /** - * Determine whether the user can delete the model. - * - * @return \Illuminate\Auth\Access\Response|bool + * Determine whether the user can bulk delete any model. */ - public function delete(User $user, Result $result): bool + public function deleteAny(User $user) { - return true; + return $user->is_admin; } /** - * Determine whether the user can delete multiple models. - * - * @return \Illuminate\Auth\Access\Response|bool + * Determine whether the user can delete the model. */ - public function deleteAny(User $user) + public function delete(User $user, Result $result): bool { - return true; + return $user->is_admin; } /** * Determine whether the user can restore the model. - * - * @return \Illuminate\Auth\Access\Response|bool */ public function restore(User $user, Result $result): bool { - // + return false; // soft deletes not used } /** * Determine whether the user can permanently delete the model. - * - * @return \Illuminate\Auth\Access\Response|bool */ public function forceDelete(User $user, Result $result): bool { - // + return false; // soft deletes not used } } diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 000000000..8b5959a5a --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,77 @@ +is_admin; + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, User $model): bool + { + return $user->is_admin; + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->is_admin; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, User $model): bool + { + if ($user->id == $model->id) { + return true; + } + + return $user->is_admin; + } + + /** + * Determine whether the user can bulk delete any model. + */ + public function deleteAny(User $user): bool + { + return false; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, User $model): bool + { + return $user->is_admin + && ! $model->is_admin; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, User $model): bool + { + return false; // soft deletes not used + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, User $model): bool + { + return false; // soft deletes not used + } +} diff --git a/composer.json b/composer.json index 4113d0704..c056a3de3 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "influxdata/influxdb-client-php": "^3.4", "laravel-notification-channels/telegram": "^4.0", "laravel/framework": "^10.22.0", + "laravel/prompts": "^0.1.6", "laravel/sanctum": "^3.3.0", "laravel/tinker": "^2.8.2", "livewire/livewire": "^3.0.2", diff --git a/composer.lock b/composer.lock index 4afdf1a87..2bfc0f831 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bc918703a67702c619b5683108121250", + "content-hash": "2b460311fff6a639d5c28d2c49023941", "packages": [ { "name": "awcodes/filament-versions", diff --git a/database/migrations/2023_09_11_225054_add_role_to_users_table.php b/database/migrations/2023_09_11_225054_add_role_to_users_table.php new file mode 100644 index 000000000..cf23b8020 --- /dev/null +++ b/database/migrations/2023_09_11_225054_add_role_to_users_table.php @@ -0,0 +1,39 @@ +string('role') + ->nullable() + ->after('remember_token'); + }); + + $user = User::first(); + + if ($user) { + $user->role = 'admin'; + + $user->save(); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('role'); + }); + } +}; diff --git a/routes/test.php b/routes/test.php index d8482c3c3..20757fd87 100644 --- a/routes/test.php +++ b/routes/test.php @@ -4,5 +4,4 @@ Route::prefix('test')->group(function () { // silence is golden -} -); +});