mirror of
https://github.com/alexjustesen/speedtest-tracker.git
synced 2026-06-23 04:20:08 +00:00
[Feature] User role (#762)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\info;
|
||||
use function Laravel\Prompts\select;
|
||||
use function Laravel\Prompts\text;
|
||||
|
||||
class UpdateUserRole extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:update-user-role';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Change the role for a given user.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$email = text(
|
||||
label: 'What is the email address?',
|
||||
required: true,
|
||||
validate: fn (string $value) => 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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@ class ResultResource extends Resource
|
||||
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 @@ class ResultResource extends Resource
|
||||
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());
|
||||
|
||||
|
||||
@@ -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 @@ class UserResource extends Resource
|
||||
->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 @@ class UserResource extends Resource
|
||||
{
|
||||
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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Models;
|
||||
|
||||
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 @@ class User extends Authenticatable implements FilamentUser
|
||||
{
|
||||
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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,11 @@ namespace App\Policies;
|
||||
|
||||
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 @@ class ResultPolicy
|
||||
|
||||
/**
|
||||
* 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 @@ class ResultPolicy
|
||||
|
||||
/**
|
||||
* 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 bulk delete any model.
|
||||
*/
|
||||
public function deleteAny(User $user)
|
||||
{
|
||||
return $user->is_admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*
|
||||
* @return \Illuminate\Auth\Access\Response|bool
|
||||
*/
|
||||
public function delete(User $user, Result $result): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete multiple models.
|
||||
*
|
||||
* @return \Illuminate\Auth\Access\Response|bool
|
||||
*/
|
||||
public function deleteAny(User $user)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Generated
+1
-1
@@ -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",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
+1
-2
@@ -4,5 +4,4 @@ use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('test')->group(function () {
|
||||
// silence is golden
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user