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,87 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Support\Permissions;
use App\Support\TranslatablePayload;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class PermissionCatalogueTest extends TestCase
{
#[Test]
public function every_role_definition_only_uses_permissions_from_the_catalogue(): void
{
$catalogue = Permissions::all();
foreach (Permissions::roleDefinitions() as $role => $permissions) {
$unknown = array_diff($permissions, $catalogue);
$this->assertSame(
[],
array_values($unknown),
"Role [{$role}] references permissions that do not exist: ".implode(', ', $unknown)
);
}
}
#[Test]
public function the_grouped_view_covers_the_whole_catalogue(): void
{
$flattened = array_merge(...array_values(Permissions::grouped()));
$this->assertEqualsCanonicalizing(Permissions::all(), $flattened);
}
#[Test]
public function super_admin_holds_no_explicit_permissions(): void
{
// Its power comes from the Gate::before hook, so the definition stays
// empty and can never fall behind the catalogue.
$this->assertSame([], Permissions::roleDefinitions()['super_admin']);
}
#[Test]
public function every_role_that_can_use_the_panel_can_reach_it(): void
{
foreach (Permissions::roleDefinitions() as $role => $permissions) {
if ($role === 'super_admin' || $permissions === []) {
continue;
}
$this->assertContains(
Permissions::PANEL_ACCESS,
$permissions,
"Role [{$role}] has permissions but cannot open the panel."
);
}
}
#[Test]
public function the_default_role_for_new_logins_exists(): void
{
$this->assertArrayHasKey(
(string) config('pireon.default_role'),
Permissions::roleDefinitions(),
);
}
#[Test]
public function a_translatable_value_can_be_submitted_as_a_plain_string(): void
{
app()->setLocale('ca');
$this->assertSame(['ca' => 'Hola'], TranslatablePayload::normalise('Hola'));
}
#[Test]
public function unsupported_locales_are_stripped_from_incoming_payloads(): void
{
$this->assertSame(
['es' => 'Hola', 'en' => 'Hi'],
TranslatablePayload::normalise(['es' => 'Hola', 'en' => 'Hi', 'de' => 'Hallo']),
);
}
}

View File

@@ -0,0 +1,186 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use PHPUnit\Framework\Attributes\Test;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use Tests\TestCase;
/**
* The three languages have to say the same things.
*
* A key present in one file and absent from another does not fail anywhere: it
* renders as the raw key — `panel.widgets.stats.projects` — on a screen nobody
* is looking at in that language. It is the kind of fault that only ever gets
* found by a user, which is why it is worth a test.
*/
class TranslationCatalogueTest extends TestCase
{
/**
* @return array<int, string>
*/
protected function locales(): array
{
return array_keys((array) config('locales.supported'));
}
/**
* Every leaf of a translation file, as dotted keys.
*
* @param array<string, mixed> $translations
* @return array<int, string>
*/
protected function flatten(array $translations, string $prefix = ''): array
{
$keys = [];
foreach ($translations as $key => $value) {
$path = $prefix === '' ? (string) $key : "{$prefix}.{$key}";
if (is_array($value)) {
$keys = [...$keys, ...$this->flatten($value, $path)];
continue;
}
$keys[] = $path;
}
return $keys;
}
/**
* @return array<string, array<string, array<int, string>>>
*/
protected function catalogue(): array
{
$catalogue = [];
foreach ($this->locales() as $locale) {
foreach (glob(lang_path("{$locale}/*.php")) ?: [] as $file) {
$catalogue[basename($file)][$locale] = $this->flatten(require $file);
}
}
return $catalogue;
}
#[Test]
public function every_language_carries_the_same_keys(): void
{
$missing = [];
foreach ($this->catalogue() as $file => $byLocale) {
$everyKey = array_unique(array_merge(...array_values($byLocale)));
foreach ($this->locales() as $locale) {
foreach (array_diff($everyKey, $byLocale[$locale] ?? []) as $key) {
$missing[] = "[{$locale}] {$file} :: {$key}";
}
}
}
$this->assertSame([], $missing, "Untranslated keys:\n".implode("\n", $missing));
}
#[Test]
public function no_translation_is_left_empty(): void
{
$empty = [];
foreach ($this->locales() as $locale) {
foreach (glob(lang_path("{$locale}/*.php")) ?: [] as $file) {
foreach (require $file as $key => $value) {
foreach ($this->leaves($value, (string) $key) as $path => $text) {
if (trim((string) $text) === '') {
$empty[] = "[{$locale}] ".basename($file)." :: {$path}";
}
}
}
}
}
$this->assertSame([], $empty, "Empty translations:\n".implode("\n", $empty));
}
/**
* Every key the application actually asks for exists.
*
* The mirror image of the test above: that one catches a key translated in
* one language and forgotten in another, this one catches a key nobody
* translated at all.
*/
#[Test]
public function every_key_the_code_uses_has_a_translation(): void
{
$default = (string) config('locales.default');
$missing = [];
foreach ($this->sourceFiles() as $file) {
$source = (string) file_get_contents($file->getPathname());
preg_match_all(
'/(?:__|trans_choice)\(\s*[\'"]([a-z0-9_]+\.[a-z0-9_.\-]+)[\'"]/i',
$source,
$matches,
);
foreach ($matches[1] as $key) {
[$group] = explode('.', $key, 2);
// Keys belonging to a package (`filament-panels::…`) or built
// dynamically are not this test's business.
if (! file_exists(lang_path("{$default}/{$group}.php"))) {
continue;
}
if (! is_string(__($key)) || __($key) === $key) {
$missing[] = $key.' ('.str_replace(base_path().'/', '', $file->getPathname()).')';
}
}
}
$this->assertSame([], array_unique($missing), "Keys with no translation:\n".implode("\n", array_unique($missing)));
}
/**
* @return array<int, SplFileInfo>
*/
protected function sourceFiles(): array
{
$files = [];
foreach ([app_path(), resource_path('views')] as $directory) {
/** @var SplFileInfo $file */
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)) as $file) {
if ($file->isFile() && in_array($file->getExtension(), ['php'], true)) {
$files[] = $file;
}
}
}
return $files;
}
/**
* @return array<string, mixed>
*/
protected function leaves(mixed $value, string $prefix): array
{
if (! is_array($value)) {
return [$prefix => $value];
}
$leaves = [];
foreach ($value as $key => $child) {
$leaves += $this->leaves($child, "{$prefix}.{$key}");
}
return $leaves;
}
}