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:
2026-08-19 17:55:27 +02:00
commit 3897113313
534 changed files with 64050 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Filament\Concerns;
use App\Support\Presence;
use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Locked;
/**
* Two people can open the same record's edit screen at once, and nothing in
* Filament's own EditRecord page says so. This is the two things that fact
* calls for.
*
* The soft warning: whoever opens the screen second is told, right away, who
* else already has it open. Only ever shown to the person arriving later —
* the first person has no reason to expect company, and there is nothing this
* screen could tell them that would help.
*
* The hard guard: whoever *saves* second is refused, if the record changed
* underneath them since their screen was opened. This is the one that
* actually prevents losing work, and it does not depend on the warning above
* having been seen or even shown — a closed tab, a second monitor, a
* teammate ten minutes ago all end up caught the same way, because it checks
* the database at the moment of saving rather than trusting what the warning
* said earlier.
*
* Applied to every Edit page rather than picked per resource: a collision is
* a property of two people and one record, not of what the record happens to
* be.
*/
trait WarnsOfConcurrentEditing
{
/**
* `updated_at` as read straight from the database when this screen was
* opened — not through the model, so a cast or a mutator cannot make two
* identical instants compare unequal.
*
* Public and Locked, both on purpose and for different reasons. Public
* because Livewire only carries public properties across requests — a
* protected one resets to null on every hydration, which silently
* disables the whole guard. Locked because a plain public property can be
* overwritten by whatever a request claims it should be; the one thing
* this value must never be is something the browser gets to supply.
*/
#[Locked]
public ?string $loadedUpdatedAt = null;
public function mount(int|string $record): void
{
parent::mount($record);
$this->loadedUpdatedAt = $this->currentUpdatedAt();
$this->warnOfOtherEditors();
}
/**
* @param array<string, mixed> $data
*/
protected function handleRecordUpdate(Model $record, array $data): Model
{
$current = $this->currentUpdatedAt();
if ($this->loadedUpdatedAt !== null && $current !== null && $current !== $this->loadedUpdatedAt) {
$this->warnOfConflict();
throw (new Halt)->rollBackDatabaseTransaction();
}
$record = parent::handleRecordUpdate($record, $data);
// What was just written is the new baseline: saving again without
// reloading is judged against what this screen itself just wrote,
// not against the moment it was first opened.
$this->loadedUpdatedAt = $this->currentUpdatedAt();
return $record;
}
protected function warnOfOtherEditors(): void
{
$user = Auth::user();
if ($user === null) {
return;
}
$others = Presence::touch($this->getRecord(), $user);
if ($others->isEmpty()) {
return;
}
Notification::make()
->warning()
->title(__('panel.presence.title'))
->body(__('panel.presence.body', ['names' => $others->pluck('name')->implode(', ')]))
->persistent()
->send();
}
protected function warnOfConflict(): void
{
Notification::make()
->danger()
->title(__('panel.concurrency.title'))
->body($this->conflictMessage())
->persistent()
->send();
}
/**
* Named when the history knows who — which is most of the time, since
* every content model records who changed what. Without a name the
* message still says everything that matters: it changed, and saving now
* would have erased that.
*/
protected function conflictMessage(): string
{
$record = $this->getRecord();
$author = method_exists($record, 'revisions')
? $record->revisions()->value('user_name')
: null;
return $author !== null
? __('panel.concurrency.body_with_author', ['name' => $author])
: __('panel.concurrency.body');
}
/**
* Read straight from the table: one column, no relations, no casts to
* disagree about, and no risk of a stale in-memory copy answering for the
* database.
*
* Precision is whole seconds, which is what `timestamps()` stores here —
* two saves inside the same second are a genuine race even in
* production, and this guard is honest about not catching that one.
*/
protected function currentUpdatedAt(): ?string
{
$record = $this->getRecord();
return DB::table($record->getTable())
->where($record->getKeyName(), $record->getKey())
->value('updated_at');
}
}