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:
182
database/seeders/DatabaseSeeder.php
Normal file
182
database/seeders/DatabaseSeeder.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Call;
|
||||
use App\Models\Category;
|
||||
use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
use App\Models\Page;
|
||||
use App\Models\Project;
|
||||
use App\Models\Repository;
|
||||
use App\Models\Service;
|
||||
use App\Models\Talk;
|
||||
use App\Models\Technology;
|
||||
use App\Models\Testimonial;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Always required: the app cannot authorise anyone without its roles
|
||||
// and permissions in place.
|
||||
$this->call(RolePermissionSeeder::class);
|
||||
|
||||
// Sample content only outside production. Real user accounts are
|
||||
// never seeded — they are created by the OAuth login flow.
|
||||
if (! app()->environment('production')) {
|
||||
$this->seedDemoContent();
|
||||
}
|
||||
}
|
||||
|
||||
protected function seedDemoContent(): void
|
||||
{
|
||||
if (Page::query()->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$categories = Category::factory()->count(4)->create();
|
||||
|
||||
Page::factory()
|
||||
->count(12)
|
||||
->published()
|
||||
->recycle($categories)
|
||||
->create();
|
||||
|
||||
Page::factory()->count(4)->draft()->recycle($categories)->create();
|
||||
|
||||
// A few pages deliberately left translated in the default locale only,
|
||||
// so the translation filters and dashboard stat have something to show.
|
||||
Page::factory()->count(3)->untranslated()->recycle($categories)->create();
|
||||
|
||||
$this->seedCollective();
|
||||
|
||||
$this->command?->info('Seeded demo content for the collective.');
|
||||
}
|
||||
|
||||
/**
|
||||
* The collective's own content: who we are, what we build, what we use and
|
||||
* what we need.
|
||||
*/
|
||||
protected function seedCollective(): void
|
||||
{
|
||||
$technologies = Technology::factory()->count(12)->create();
|
||||
$members = Member::factory()->count(8)->create();
|
||||
|
||||
Member::factory()->count(2)->founder()->create();
|
||||
Member::factory()->count(2)->past()->create();
|
||||
|
||||
Member::all()->each(
|
||||
fn (Member $member) => $member->technologies()->sync(
|
||||
$technologies->random(rand(2, 5))->pluck('id'),
|
||||
),
|
||||
);
|
||||
|
||||
$projects = Project::factory()
|
||||
->count(6)
|
||||
->published()
|
||||
->create()
|
||||
->each(function (Project $project) use ($technologies, $members): void {
|
||||
$project->technologies()->sync($technologies->random(rand(2, 5))->pluck('id'));
|
||||
$project->members()->sync(
|
||||
$members->random(rand(1, 3))->mapWithKeys(fn (Member $m) => [
|
||||
$m->id => ['role' => fake()->randomElement(['dev', 'diseño', 'infra'])],
|
||||
]),
|
||||
);
|
||||
$project->update(['lead_member_id' => $members->random()->id]);
|
||||
|
||||
Repository::factory()->for($project)->create();
|
||||
});
|
||||
|
||||
// One archived project with a write-up, and one unpublished, so the
|
||||
// public API's filtering has something real to exclude.
|
||||
Project::factory()->withPostmortem()->published()->create();
|
||||
Project::factory()->draft()->create();
|
||||
|
||||
$this->seedShowcase($projects, $members, $technologies);
|
||||
|
||||
// Open calls, some attached to a project and some standalone.
|
||||
Call::factory()
|
||||
->count(4)
|
||||
->create(['project_id' => $projects->random()->id])
|
||||
->each(fn (Call $call) => $call->technologies()->sync(
|
||||
$technologies->random(rand(1, 3))->pluck('id'),
|
||||
));
|
||||
|
||||
Call::factory()->count(2)->create(['project_id' => null]);
|
||||
Call::factory()->closed()->create(['project_id' => null]);
|
||||
|
||||
// Mirrored issues, so the "good first issues" endpoint is not empty
|
||||
// before anyone has configured a real git token.
|
||||
Repository::all()->each(function (Repository $repository): void {
|
||||
foreach (range(1, rand(1, 4)) as $number) {
|
||||
$repository->issues()->create([
|
||||
'number' => $number,
|
||||
'title' => fake()->sentence(6),
|
||||
'html_url' => "{$repository->html_url}/issues/{$number}",
|
||||
'labels' => ['good first issue'],
|
||||
'is_good_first_issue' => true,
|
||||
'is_help_wanted' => fake()->boolean(),
|
||||
'author' => 'maintainer',
|
||||
'assignee' => fake()->boolean(25) ? 'someone' : null,
|
||||
'comments' => rand(0, 8),
|
||||
'issue_created_at' => now()->subDays(rand(1, 90)),
|
||||
'issue_updated_at' => now()->subDays(rand(0, 10)),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* What the landing page is built from.
|
||||
*
|
||||
* Without these a fresh install has projects and members but nothing to
|
||||
* compose a front page out of, which makes it impossible to tell whether
|
||||
* the frontend works.
|
||||
*
|
||||
* @param Collection<int, Project> $projects
|
||||
* @param Collection<int, Member> $members
|
||||
* @param Collection<int, Technology> $technologies
|
||||
*/
|
||||
protected function seedShowcase($projects, $members, $technologies): void
|
||||
{
|
||||
// Organisations the work was for, each with at least one project so
|
||||
// the wall of logos is not empty.
|
||||
$clients = Client::factory()->count(4)->create();
|
||||
|
||||
$projects->each(fn (Project $project) => $project->update([
|
||||
'client_id' => $clients->random()->id,
|
||||
]));
|
||||
|
||||
$services = Service::factory()->count(4)->create();
|
||||
|
||||
$services->each(function (Service $service) use ($projects, $technologies): void {
|
||||
$service->technologies()->sync($technologies->random(rand(2, 4))->pluck('id'));
|
||||
$service->projects()->sync($projects->random(rand(1, 3))->pluck('id'));
|
||||
});
|
||||
|
||||
// One published service with nothing behind it, so the health check
|
||||
// that calls that out has something real to find.
|
||||
Service::factory()->create(['is_featured' => true]);
|
||||
|
||||
Service::factory()->draft()->create();
|
||||
|
||||
Testimonial::factory()->count(3)->create([
|
||||
'client_id' => $clients->random()->id,
|
||||
]);
|
||||
|
||||
Testimonial::factory()->create(['service_id' => $services->random()->id]);
|
||||
Testimonial::factory()->create(['project_id' => $projects->random()->id]);
|
||||
|
||||
Talk::factory()->count(5)->create([
|
||||
'member_id' => $members->random()->id,
|
||||
]);
|
||||
|
||||
// And one nobody is credited with, for the same reason.
|
||||
Talk::factory()->create(['member_id' => null]);
|
||||
}
|
||||
}
|
||||
54
database/seeders/RolePermissionSeeder.php
Normal file
54
database/seeders/RolePermissionSeeder.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Support\Permissions;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
|
||||
/**
|
||||
* Brings the database in line with App\Support\Permissions.
|
||||
*
|
||||
* Idempotent: safe to re-run after every deploy. Permissions removed from the
|
||||
* catalogue are deleted, so a permission that no longer exists in code cannot
|
||||
* keep granting access.
|
||||
*/
|
||||
class RolePermissionSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
|
||||
$catalogue = Permissions::all();
|
||||
|
||||
foreach ($catalogue as $name) {
|
||||
Permission::findOrCreate($name, 'web');
|
||||
}
|
||||
|
||||
// Drop permissions that were retired from the catalogue, along with
|
||||
// their role and user assignments.
|
||||
Permission::query()->whereNotIn('name', $catalogue)->delete();
|
||||
|
||||
foreach (Permissions::roleDefinitions() as $roleName => $permissions) {
|
||||
$role = Role::findOrCreate($roleName, 'web');
|
||||
|
||||
// syncPermissions is what makes this idempotent: the role ends up
|
||||
// with exactly the definition in code, no more and no less.
|
||||
// super_admin gets an empty set on purpose — its power comes from
|
||||
// the Gate::before hook in AuthServiceProvider.
|
||||
$role->syncPermissions($permissions);
|
||||
}
|
||||
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
|
||||
$this->command?->info(sprintf(
|
||||
'Synced %d permissions across %d roles.',
|
||||
count($catalogue),
|
||||
count(Permissions::roleDefinitions()),
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user