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,76 @@
<?php
declare(strict_types=1);
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Validates a translatable field submitted either as a plain string (meaning
* "the current locale") or as a locale => value object.
*
* Unknown locale keys are rejected rather than ignored, so a typo like "gb"
* fails loudly instead of silently writing a translation nobody will ever read.
*/
class Translatable implements ValidationRule
{
public function __construct(
protected bool $requireDefaultLocale = false,
protected ?int $max = null,
) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$supported = array_keys(config('locales.supported'));
$default = (string) config('locales.default');
if (is_string($value)) {
$value = [app()->getLocale() => $value];
}
if (! is_array($value)) {
$fail(__('rules.translatable.type', ['attribute' => $attribute]));
return;
}
foreach ($value as $locale => $translation) {
if (! in_array((string) $locale, $supported, true)) {
$fail(__('rules.translatable.unsupported_locale', [
'attribute' => $attribute,
'locale' => (string) $locale,
'supported' => implode(', ', $supported),
]));
return;
}
if ($translation !== null && ! is_string($translation)) {
$fail(__('rules.translatable.type', ['attribute' => $attribute]));
return;
}
if ($this->max !== null && is_string($translation) && mb_strlen($translation) > $this->max) {
$fail(__('rules.translatable.max', [
'attribute' => $attribute,
'locale' => (string) $locale,
'max' => $this->max,
]));
return;
}
}
// The default locale is the fallback every other language borrows from
// when it has no translation, so a record without it can render blank.
if ($this->requireDefaultLocale && blank($value[$default] ?? null)) {
$fail(__('rules.translatable.default_required', [
'attribute' => $attribute,
'locale' => $default,
]));
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Model;
/**
* Enforces slug uniqueness per locale.
*
* Slugs live inside a JSON map, which no database unique index can cover, so
* the check happens here: for each locale in the payload, no *other* row may
* already use that slug in that same locale. The same slug in two different
* languages is fine.
*/
class UniqueTranslatedSlug implements ValidationRule
{
/**
* @param class-string<Model> $model
*/
public function __construct(
protected string $model,
protected ?int $ignoreId = null,
protected string $column = 'slug',
) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$translations = is_string($value)
? [app()->getLocale() => $value]
: (array) $value;
foreach ($translations as $locale => $slug) {
if (blank($slug)) {
continue;
}
$query = $this->model::query()
->where("{$this->column}->{$locale}", $slug);
if ($this->ignoreId !== null) {
$query->whereKeyNot($this->ignoreId);
}
// Soft-deleted rows still hold their slug, so include them —
// otherwise restoring one would resurrect a duplicate.
if (method_exists($this->model, 'bootSoftDeletes')) {
$query->withTrashed();
}
if ($query->exists()) {
$fail(__('rules.slug.unique', [
'slug' => $slug,
'locale' => (string) $locale,
]));
return;
}
}
}
}