Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
15
app/Filament/Resources/Roles/Pages/CreateRole.php
Normal file
15
app/Filament/Resources/Roles/Pages/CreateRole.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources\Roles\Pages;
|
||||
|
||||
use App\Filament\Resources\Roles\RoleResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateRole extends CreateRecord
|
||||
{
|
||||
use HandlesRolePermissions;
|
||||
|
||||
protected static string $resource = RoleResource::class;
|
||||
}
|
||||
24
app/Filament/Resources/Roles/Pages/EditRole.php
Normal file
24
app/Filament/Resources/Roles/Pages/EditRole.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources\Roles\Pages;
|
||||
|
||||
use App\Filament\Concerns\WarnsOfConcurrentEditing;
|
||||
use App\Filament\Resources\Roles\RoleResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditRole extends EditRecord
|
||||
{
|
||||
use HandlesRolePermissions, WarnsOfConcurrentEditing;
|
||||
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
100
app/Filament/Resources/Roles/Pages/HandlesRolePermissions.php
Normal file
100
app/Filament/Resources/Roles/Pages/HandlesRolePermissions.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources\Roles\Pages;
|
||||
|
||||
use App\Support\Permissions;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
|
||||
/**
|
||||
* Bridges the form's per-group checkbox state and the role's single
|
||||
* `permissions` relationship.
|
||||
*
|
||||
* The form splits permissions across one component per resource for
|
||||
* readability; the database wants one flat set. This trait converts in both
|
||||
* directions and keeps the two in step.
|
||||
*/
|
||||
trait HandlesRolePermissions
|
||||
{
|
||||
/**
|
||||
* Permission names captured on the way to saving, held here because the
|
||||
* relationship can only be synced once the role row exists.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $selectedPermissions = [];
|
||||
|
||||
/**
|
||||
* Expand the role's flat permission set into the form's grouped shape.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
$held = $this->getRecord()->permissions->pluck('name')->all();
|
||||
|
||||
foreach (Permissions::grouped() as $group => $permissions) {
|
||||
$data['permission_groups'][$group] = array_values(array_intersect($permissions, $held));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
return $this->extractPermissions($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
return $this->extractPermissions($data);
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->syncSelectedPermissions();
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$this->syncSelectedPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function extractPermissions(array $data): array
|
||||
{
|
||||
$groups = $data['permission_groups'] ?? [];
|
||||
unset($data['permission_groups']);
|
||||
|
||||
// Intersecting with the catalogue means a stale or hand-crafted value
|
||||
// in the payload cannot grant a permission the app does not define.
|
||||
$this->selectedPermissions = array_values(array_intersect(
|
||||
array_merge(...array_values(array_map(fn ($v) => (array) $v, $groups)) ?: [[]]),
|
||||
Permissions::all(),
|
||||
));
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function syncSelectedPermissions(): void
|
||||
{
|
||||
$this->getRecord()->syncPermissions($this->selectedPermissions);
|
||||
|
||||
// spatie caches the permission map; without this the change would not
|
||||
// take effect until the cache expired.
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
}
|
||||
}
|
||||
21
app/Filament/Resources/Roles/Pages/ListRoles.php
Normal file
21
app/Filament/Resources/Roles/Pages/ListRoles.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources\Roles\Pages;
|
||||
|
||||
use App\Filament\Resources\Roles\RoleResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListRoles extends ListRecords
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user