admin user policies
Some checks failed
linter / quality (push) Successful in 3m5s
tests / ci (push) Has been cancelled

This commit is contained in:
Javier Feliz 2025-08-01 22:29:35 -04:00
parent 93c6baa16b
commit f7ee4baa61
12 changed files with 281 additions and 14 deletions

View File

@ -15,7 +15,10 @@ class AppContainer extends Component
public function mount()
{
$this->loadApps();
// Only load data if user is authorized to view it
if (auth()->user()->can('viewAny', Application::class)) {
$this->loadApps();
}
}
public function loadApps()
@ -38,6 +41,8 @@ class AppContainer extends Component
public function deleteApp()
{
$this->authorize('delete', $this->confirmDeleteApp);
$this->confirmDeleteApp->delete();
$deletedId = $this->confirmDeleteApp->id;
$this->confirmDeleteApp = null;

View File

@ -16,6 +16,8 @@ class NewApplication extends Component
public function create()
{
$this->authorize('create', Application::class);
$this->validate();
Application::create([

View File

@ -16,12 +16,17 @@ class ManageUsers extends Component
public function mount()
{
$this->users = User::all();
$this->invitations = Invitation::all();
// Only load data if user is authorized to view it
if (auth()->user()->can('viewAny', User::class)) {
$this->users = User::all();
$this->invitations = Invitation::all();
}
}
public function inviteUser()
{
$this->authorize('invite', User::class);
$inv = Invitation::create([
'code' => str()->random(50),
'email' => $this->invite_email,
@ -29,6 +34,20 @@ class ManageUsers extends Component
'expires_at' => now()->addDays(7),
]);
Flux::modal('invite-user')->close();
// Refresh the data
$this->invitations = Invitation::all();
$this->invite_email = '';
}
public function deleteUser(User $user)
{
$this->authorize('delete', $user);
$user->delete();
// Refresh the data
$this->users = User::all();
}
public function render()

View File

@ -24,7 +24,8 @@ class User extends Authenticatable
'email',
'password',
'avatar',
'preferred_username'
'preferred_username',
'is_admin'
];
/**
@ -47,6 +48,7 @@ class User extends Authenticatable
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_admin' => 'boolean',
];
}
@ -71,4 +73,9 @@ class User extends Authenticatable
{
return route('user.avatar', ['path' => $this->avatar]);
}
public function isAdmin(): bool
{
return $this->is_admin;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Policies;
use App\Models\Application;
use App\Models\User;
class ApplicationPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->isAdmin();
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Application $application): bool
{
return $user->isAdmin();
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->isAdmin();
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Application $application): bool
{
return $user->isAdmin();
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Application $application): bool
{
return $user->isAdmin();
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Application $application): bool
{
return $user->isAdmin();
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Application $application): bool
{
return $user->isAdmin();
}
}

View File

@ -0,0 +1,75 @@
<?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->isAdmin();
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, User $model): bool
{
// Users can view their own profile, or admins can view any user
return $user->id === $model->id || $user->isAdmin();
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->isAdmin();
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, User $model): bool
{
// Users can update their own profile, or admins can update any user
return $user->id === $model->id || $user->isAdmin();
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, User $model): bool
{
// Only admins can delete users, and they cannot delete themselves
return $user->isAdmin() && $user->id !== $model->id;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, User $model): bool
{
return $user->isAdmin();
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, User $model): bool
{
return $user->isAdmin() && $user->id !== $model->id;
}
/**
* Determine whether the user can invite new users.
*/
public function invite(User $user): bool
{
return $user->isAdmin();
}
}

View File

@ -29,6 +29,7 @@ class UserFactory extends Factory
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
'is_admin' => false,
];
}
@ -41,4 +42,14 @@ class UserFactory extends Factory
'email_verified_at' => null,
]);
}
/**
* Indicate that the user should be an admin.
*/
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'is_admin' => true,
]);
}
}

View File

@ -0,0 +1,28 @@
<?php
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->boolean('is_admin')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
}
};

View File

@ -12,5 +12,24 @@ class UserSeeder extends Seeder
/**
* Run the database seeds.
*/
public function run(): void {}
public function run(): void
{
// Create an admin user
User::create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
'is_admin' => true,
]);
// Create a regular user
User::create([
'name' => 'Regular User',
'email' => 'user@example.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
'is_admin' => false,
]);
}
}

View File

@ -1,16 +1,36 @@
<x-layouts.app :title="__('Dashboard')">
<div class="max-w-4xl mx-auto py-12">
<div class="mb-4">
<livewire:manage-users />
</div>
@can('viewAny', App\Models\User::class)
<div class="mb-4">
<livewire:manage-users />
</div>
@endcan
<div class="grid grid-cols-2">
<livewire:forms.user-profile />
</div>
<div class="mt-4">
<livewire:manage-authentication-tokens />
</div>
<div class="mt-4">
<livewire:app-container />
</div>
@can('viewAny', App\Models\Application::class)
<div class="mt-4">
<livewire:app-container />
</div>
@endcan
@cannot('viewAny', App\Models\Application::class)
<div class="mt-4">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<h3 class="text-lg font-medium">Welcome to AuthentiKate</h3>
<p class="mt-2 text-sm text-gray-600">
You can manage your profile and view your authentication tokens above.
</p>
</div>
</div>
</div>
@endcannot
</div>
</x-layouts.app>

View File

@ -1,17 +1,22 @@
@can('viewAny', App\Models\Application::class)
<div>
<div class="flex items-center justify-end mb-8">
@can('create', App\Models\Application::class)
<flux:modal.trigger name="new-app">
<flux:button variant="primary" icon="plus">New App</flux:button>
</flux:modal.trigger>
@endcan
</div>
<div class="mt-4 grid grid-cols-4 gap-8">
@foreach ($apps as $app)
<div class="p-4 flex flex-col gap-4 rounded-md bg-gray-100 dark:bg-zinc-700">
<div class="flex items-center justify-between">
@can('view', $app)
<flux:modal.trigger name="app-info">
<flux:button icon="eye" variant="subtle" size="sm" inset class="cursor-pointer"
x-on:click="$dispatch('appinfo', {id: {{$app->id}}})" />
</flux:modal.trigger>
@endcan
<flux:button wire:click="confirmDelete({{$app->id}})" icon="trash" variant="subtle" size="sm" inset
class="cursor-pointer" />
</div>
@ -30,5 +35,8 @@
@endif
</flux:modal>
<livewire:app-info-modal />
@can('create', App\Models\Application::class)
<livewire:forms.new-application />
</div>
@endcan
</div>
@endcan

View File

@ -1,3 +1,4 @@
@can('viewAny', App\Models\User::class)
<div>
<div class="flex justify-between items-center">
<flux:heading size="xl">Users</flux:heading>
@ -9,15 +10,20 @@
<flux:heading>{{$u->name}}</flux:heading>
<flux:text>{{$u->email}}</flux:text>
</div>
@can('delete', $u)
<flux:button variant="danger" size="sm" wire:click="deleteUser({{ $u->id }})">Delete</flux:button>
@endcan
</x-card>
@endforeach
<div class="flex justify-between items-center mt-8">
<flux:heading size="xl">Invitations</flux:heading>
@can('invite', App\Models\User::class)
<div>
<flux:modal.trigger name="invite-user">
<flux:button variant="primary" icon="plus">Create</flux:button>
</flux:modal.trigger>
</div>
@endcan
</div>
<flux:separator class="my-8" />
@foreach ($invitations as $inv)
@ -35,12 +41,12 @@
<flux:badge>{{$inv->status}}</flux:badge>
@endswitch
</div>
<flux:text>Invite link: {{route('register', ['code' => $inv->code])}}</flux:text>
<div class="flex gap-4 items-center">
<flux:button variant="primary" size="sm">Copy invite link</flux:button>
</div>
</x-card>
@endforeach
@can('invite', App\Models\User::class)
<flux:modal name="invite-user" class="w-96">
<flux:heading>Invite User</flux:heading>
<flux:separator class="my-4" />
@ -49,4 +55,6 @@
<flux:button type="submit" variant="primary">Create invitation</flux:button>
</form>
</flux:modal>
</div>
@endcan
</div>
@endcan