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>
87 lines
2.6 KiB
PHP
87 lines
2.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Pireon;
|
|
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Contracts\Support\Arrayable;
|
|
|
|
/**
|
|
* A token set returned by the identity server's /oauth/token endpoint.
|
|
*
|
|
* @implements Arrayable<string, mixed>
|
|
*/
|
|
final readonly class AccessToken implements Arrayable
|
|
{
|
|
public function __construct(
|
|
public string $accessToken,
|
|
public ?string $refreshToken,
|
|
public ?CarbonImmutable $expiresAt,
|
|
public string $tokenType = 'Bearer',
|
|
/** @var array<int, string> */
|
|
public array $scopes = [],
|
|
) {}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
public static function fromResponse(array $payload): self
|
|
{
|
|
$expiresIn = isset($payload['expires_in']) ? (int) $payload['expires_in'] : null;
|
|
|
|
return new self(
|
|
accessToken: (string) $payload['access_token'],
|
|
refreshToken: isset($payload['refresh_token']) ? (string) $payload['refresh_token'] : null,
|
|
// Shave a minute off so we refresh slightly before the server
|
|
// would start rejecting the token mid-request.
|
|
expiresAt: $expiresIn !== null ? CarbonImmutable::now()->addSeconds($expiresIn)->subMinute() : null,
|
|
tokenType: (string) ($payload['token_type'] ?? 'Bearer'),
|
|
scopes: array_values(array_filter(explode(' ', (string) ($payload['scope'] ?? '')))),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $state
|
|
*/
|
|
public static function fromArray(array $state): self
|
|
{
|
|
return new self(
|
|
accessToken: (string) $state['access_token'],
|
|
refreshToken: $state['refresh_token'] ?? null,
|
|
expiresAt: isset($state['expires_at']) ? CarbonImmutable::parse($state['expires_at']) : null,
|
|
tokenType: (string) ($state['token_type'] ?? 'Bearer'),
|
|
scopes: $state['scopes'] ?? [],
|
|
);
|
|
}
|
|
|
|
public function hasExpired(): bool
|
|
{
|
|
return $this->expiresAt !== null && $this->expiresAt->isPast();
|
|
}
|
|
|
|
public function isRefreshable(): bool
|
|
{
|
|
return $this->refreshToken !== null;
|
|
}
|
|
|
|
public function hasScope(string $scope): bool
|
|
{
|
|
return in_array($scope, $this->scopes, true);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
return [
|
|
'access_token' => $this->accessToken,
|
|
'refresh_token' => $this->refreshToken,
|
|
'expires_at' => $this->expiresAt?->toIso8601String(),
|
|
'token_type' => $this->tokenType,
|
|
'scopes' => $this->scopes,
|
|
];
|
|
}
|
|
}
|