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:
97
app/Http/Controllers/Api/V1/TokenController.php
Normal file
97
app/Http/Controllers/Api/V1/TokenController.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Actions\Auth\SyncPireonUser;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Pireon\PireonClient;
|
||||
use App\Services\Pireon\PireonException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/token
|
||||
*
|
||||
* A thin, rate-limited proxy over the identity server's password and
|
||||
* refresh_token grants, so first-party clients (mobile apps, test suites) can
|
||||
* obtain a token without embedding the OAuth server's URL and client secret.
|
||||
*
|
||||
* The panel's sign-in form uses the same grant directly, so
|
||||
* `PIREON_PASSWORD_GRANT=false` closes both doors at once.
|
||||
*/
|
||||
class TokenController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected PireonClient $client,
|
||||
protected SyncPireonUser $sync,
|
||||
) {}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'grant_type' => ['required', 'in:password,refresh_token'],
|
||||
'username' => ['required_if:grant_type,password', 'string'],
|
||||
'password' => ['required_if:grant_type,password', 'string'],
|
||||
'refresh_token' => ['required_if:grant_type,refresh_token', 'string'],
|
||||
'scope' => ['sometimes', 'string'],
|
||||
]);
|
||||
|
||||
$scopes = isset($data['scope'])
|
||||
? array_values(array_filter(explode(' ', $data['scope'])))
|
||||
: null;
|
||||
|
||||
try {
|
||||
$token = $data['grant_type'] === 'password'
|
||||
? $this->client->requestPasswordToken($data['username'], $data['password'], $scopes)
|
||||
: $this->client->refresh($data['refresh_token']);
|
||||
|
||||
$identity = $this->client->identity($token->accessToken);
|
||||
} catch (PireonException $e) {
|
||||
// An identity server that failed to answer has said nothing about
|
||||
// these credentials, so it must not be reported as though it had.
|
||||
if (! $e->isRejection()) {
|
||||
return response()->json([
|
||||
'message' => __('pireon.server_unavailable'),
|
||||
], Response::HTTP_SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
// Otherwise a validation error rather than the upstream body, so
|
||||
// we never echo details of the identity server back to a caller
|
||||
// probing for valid accounts.
|
||||
throw ValidationException::withMessages([
|
||||
'username' => [__('auth.failed')],
|
||||
])->status($e->status === 401 ? 401 : 422);
|
||||
}
|
||||
|
||||
if (! $identity->isUsable()) {
|
||||
return response()->json([
|
||||
'message' => $identity->isApproved()
|
||||
? __('pireon.inactive')
|
||||
: __('pireon.pending_approval'),
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Provision the local account now, so the very first API call after
|
||||
// this one already has roles and a user row to authorise against.
|
||||
$user = $this->sync->handle($identity);
|
||||
|
||||
return response()->json([
|
||||
'access_token' => $token->accessToken,
|
||||
'refresh_token' => $token->refreshToken,
|
||||
'token_type' => $token->tokenType,
|
||||
'expires_at' => $token->expiresAt?->toIso8601String(),
|
||||
'scopes' => $token->scopes,
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'roles' => $user->roles->pluck('name')->values(),
|
||||
'permissions' => $user->getAllPermissions()->pluck('name')->values(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user