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:
81
app/Actions/Auth/SyncPireonUser.php
Normal file
81
app/Actions/Auth/SyncPireonUser.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\Pireon\PireonIdentity;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
/**
|
||||
* Mirror an identity from the OAuth server into the local users table.
|
||||
*
|
||||
* Identity fields are overwritten on every login — the identity server is the
|
||||
* source of truth for who someone is. Local roles are never touched here: this
|
||||
* application owns authorisation, so a role granted in the panel survives every
|
||||
* subsequent login, and `app_roles` reported upstream grant nothing.
|
||||
*/
|
||||
class SyncPireonUser
|
||||
{
|
||||
public function handle(PireonIdentity $identity): User
|
||||
{
|
||||
return DB::transaction(function () use ($identity): User {
|
||||
$user = User::query()
|
||||
->where('oauth_id', $identity->id)
|
||||
->first();
|
||||
|
||||
// Adopt a pre-existing local account created by a seeder or an
|
||||
// earlier import, so the first real login claims it instead of
|
||||
// colliding with the unique email index.
|
||||
$user ??= User::query()
|
||||
->whereNull('oauth_id')
|
||||
->where('email', $identity->email)
|
||||
->first();
|
||||
|
||||
$isNew = $user === null;
|
||||
$user ??= new User;
|
||||
|
||||
$user->fill([
|
||||
'oauth_id' => $identity->id,
|
||||
'name' => $identity->name,
|
||||
'email' => $identity->email,
|
||||
'is_active' => $identity->isActive,
|
||||
'remote_roles' => $identity->roles,
|
||||
'last_login_at' => now(),
|
||||
]);
|
||||
|
||||
// The upstream account is already verified by the identity server;
|
||||
// recording it here keeps Laravel's own checks satisfied.
|
||||
$user->email_verified_at ??= now();
|
||||
|
||||
$user->save();
|
||||
|
||||
if ($isNew) {
|
||||
$this->assignDefaultRole($user);
|
||||
}
|
||||
|
||||
return $user->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A brand new identity gets the least-privileged role. Promoting them is a
|
||||
* deliberate act performed by an administrator in the panel.
|
||||
*/
|
||||
protected function assignDefaultRole(User $user): void
|
||||
{
|
||||
$role = (string) config('pireon.default_role');
|
||||
|
||||
if (blank($role) || $user->roles()->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Silently skip when the role has not been seeded yet, so a login can
|
||||
// never fail because of a missing local role.
|
||||
if (Role::where('name', $role)->exists()) {
|
||||
$user->assignRole($role);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user