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,90 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Asking somebody to look at something before it goes live.
*
* `pages.publish` has always been a permission of its own, because putting
* something on the website is a bigger act than writing it. What was missing
* was the other half: somebody holding only `pages.update` could write a draft
* and then had **no way to say it was ready**. The status field that would
* have said so is disabled for exactly those users, so the handover happened
* in a chat message, or did not happen at all.
*
* Two pieces. The columns hold the current state — is this waiting, who asked,
* who was asked — because that is what a query needs to answer "what is
* waiting for me". The table holds the conversation, which is a different
* thing from the change history: revisions record what the text used to say,
* and none of them can record "the photo is missing".
*
* Only on the three models where publishing is a separate permission. Adding
* review to a testimonial would be ceremony: whoever may write one may publish
* it, so there is nobody to ask.
*/
return new class extends Migration
{
/**
* @var array<int, string>
*/
protected array $tables = ['pages', 'projects', 'services'];
public function up(): void
{
foreach ($this->tables as $table) {
Schema::table($table, function (Blueprint $table) {
// The single source of truth for "is this waiting for
// somebody". Null means it is not.
$table->timestamp('review_requested_at')->nullable();
$table->foreignId('review_requested_by')->nullable()->constrained('users')->nullOnDelete();
// Optional. A review nobody is named for goes to everyone who
// could publish it — better than a queue addressed to nobody,
// and better than forcing the author to guess who is free.
$table->foreignId('reviewer_id')->nullable()->constrained('users')->nullOnDelete();
$table->index('review_requested_at');
});
}
Schema::create('review_comments', function (Blueprint $table) {
$table->id();
$table->morphs('reviewable');
// Nullable, like a revision's: work sent back by a command or a
// scheduled task belongs to nobody, and blaming the last person to
// sign in would be worse than saying so.
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('user_name')->nullable();
$table->string('event', 20);
// Required when sending work back, optional everywhere else.
// Returning somebody's work without saying why is the thing that
// makes a review process resented.
$table->text('body')->nullable();
$table->timestamps();
$table->index(['reviewable_type', 'reviewable_id', 'id']);
});
}
public function down(): void
{
Schema::dropIfExists('review_comments');
foreach ($this->tables as $table) {
Schema::table($table, function (Blueprint $table) {
$table->dropConstrainedForeignKey('review_requested_by');
$table->dropConstrainedForeignKey('reviewer_id');
$table->dropColumn('review_requested_at');
});
}
}
};