new funcionality
This commit is contained in:
220
app/Http/Controllers/Api/WebhookController.php
Normal file
220
app/Http/Controllers/Api/WebhookController.php
Normal file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\SendWebhookJob;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\User;
|
||||
use App\Models\WebhookDelivery;
|
||||
use App\Models\WebhookEndpoint;
|
||||
use App\Rules\SafeWebhookUrl;
|
||||
use App\Support\WebhookEndpoints;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||
use Illuminate\Routing\Controllers\Middleware;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Passport\Client;
|
||||
use Laravel\Passport\Http\Middleware\CheckToken;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class WebhookController extends Controller implements HasMiddleware
|
||||
{
|
||||
public static function middleware(): array
|
||||
{
|
||||
return [
|
||||
new Middleware(CheckToken::using('account:read'), only: ['index', 'deliveries']),
|
||||
new Middleware(CheckToken::using('account:write'), only: ['store', 'update', 'destroy', 'retryDelivery']),
|
||||
];
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/clients/{client}/webhooks',
|
||||
summary: 'Listar los webhooks registrados en uno de tus clientes OAuth',
|
||||
security: [['oauth2' => ['account:read']]],
|
||||
tags: ['Account'],
|
||||
parameters: [new OA\Parameter(name: 'client', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Listado de webhooks'),
|
||||
new OA\Response(response: 404, description: 'No es tuyo o no existe'),
|
||||
]
|
||||
)]
|
||||
public function index(Request $request, Client $client)
|
||||
{
|
||||
abort_unless($client->owner_id === $this->requireUser($request)->id, 404);
|
||||
|
||||
return response()->json(
|
||||
WebhookEndpoint::where('client_id', $client->id)
|
||||
->latest('created_at')
|
||||
->get()
|
||||
->map(fn (WebhookEndpoint $endpoint) => WebhookEndpoints::present($endpoint))
|
||||
);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/clients/{client}/webhooks',
|
||||
summary: 'Registrar un webhook nuevo en uno de tus clientes OAuth',
|
||||
description: 'Eventos disponibles: token.revoked (se revocó un token de este client), user.deactivated (un usuario con token vigente para este client fue desactivado). El secret se devuelve una sola vez, usalo para verificar la firma HMAC-SHA256 en el header X-Webhook-Signature.',
|
||||
security: [['oauth2' => ['account:write']]],
|
||||
tags: ['Account'],
|
||||
parameters: [new OA\Parameter(name: 'client', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['url', 'events'],
|
||||
properties: [
|
||||
new OA\Property(property: 'url', type: 'string', format: 'uri'),
|
||||
new OA\Property(property: 'events', type: 'array', items: new OA\Items(type: 'string', enum: ['token.revoked', 'user.deactivated'])),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Webhook creado (incluye secret una sola vez)'),
|
||||
new OA\Response(response: 404, description: 'No es tuyo o no existe'),
|
||||
]
|
||||
)]
|
||||
public function store(Request $request, Client $client)
|
||||
{
|
||||
abort_unless($client->owner_id === $this->requireUser($request)->id, 404);
|
||||
|
||||
$data = $request->validate([
|
||||
'url' => ['required', 'url', 'max:2048', new SafeWebhookUrl()],
|
||||
'events' => ['required', 'array', 'min:1'],
|
||||
'events.*' => [Rule::in(WebhookEndpoints::EVENTS)],
|
||||
]);
|
||||
|
||||
$secret = Str::random(40);
|
||||
|
||||
$endpoint = WebhookEndpoint::create([
|
||||
'client_id' => $client->id,
|
||||
'url' => $data['url'],
|
||||
'secret' => $secret,
|
||||
'events' => $data['events'],
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
AuditLog::record($request, 'client.webhook_created', 'client', $client->id, $client->name, [
|
||||
'url' => $data['url'],
|
||||
'events' => $data['events'],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
...WebhookEndpoints::present($endpoint),
|
||||
'secret' => $secret,
|
||||
], 201);
|
||||
}
|
||||
|
||||
#[OA\Put(
|
||||
path: '/clients/{client}/webhooks/{webhook}',
|
||||
summary: 'Editar un webhook (url, eventos o activo/pausado)',
|
||||
security: [['oauth2' => ['account:write']]],
|
||||
tags: ['Account'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'client', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'webhook', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Webhook actualizado'),
|
||||
new OA\Response(response: 404, description: 'No es tuyo o no existe'),
|
||||
]
|
||||
)]
|
||||
public function update(Request $request, Client $client, WebhookEndpoint $webhook)
|
||||
{
|
||||
abort_unless($client->owner_id === $this->requireUser($request)->id, 404);
|
||||
abort_unless($webhook->client_id === $client->id, 404);
|
||||
|
||||
$data = $request->validate([
|
||||
'url' => ['sometimes', 'url', 'max:2048', new SafeWebhookUrl()],
|
||||
'events' => ['sometimes', 'array', 'min:1'],
|
||||
'events.*' => [Rule::in(WebhookEndpoints::EVENTS)],
|
||||
'active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$webhook->update($data);
|
||||
|
||||
return response()->json(WebhookEndpoints::present($webhook->fresh()));
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/clients/{client}/webhooks/{webhook}',
|
||||
summary: 'Borrar un webhook',
|
||||
security: [['oauth2' => ['account:write']]],
|
||||
tags: ['Account'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'client', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'webhook', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 204, description: 'Webhook borrado'),
|
||||
new OA\Response(response: 404, description: 'No es tuyo o no existe'),
|
||||
]
|
||||
)]
|
||||
public function destroy(Request $request, Client $client, WebhookEndpoint $webhook)
|
||||
{
|
||||
abort_unless($client->owner_id === $this->requireUser($request)->id, 404);
|
||||
abort_unless($webhook->client_id === $client->id, 404);
|
||||
|
||||
AuditLog::record($request, 'client.webhook_deleted', 'client', $client->id, $client->name, [
|
||||
'url' => $webhook->url,
|
||||
]);
|
||||
|
||||
$webhook->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/clients/{client}/webhooks/{webhook}/deliveries',
|
||||
summary: 'Historial paginado de intentos de entrega de un webhook',
|
||||
security: [['oauth2' => ['account:read']]],
|
||||
tags: ['Account'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'client', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'webhook', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Listado paginado de entregas'),
|
||||
new OA\Response(response: 404, description: 'No es tuyo o no existe'),
|
||||
]
|
||||
)]
|
||||
public function deliveries(Request $request, Client $client, WebhookEndpoint $webhook)
|
||||
{
|
||||
abort_unless($client->owner_id === $this->requireUser($request)->id, 404);
|
||||
abort_unless($webhook->client_id === $client->id, 404);
|
||||
|
||||
return response()->json(WebhookDelivery::paginatedFor($webhook, $request));
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/clients/{client}/webhooks/{webhook}/deliveries/{delivery}/retry',
|
||||
summary: 'Reintentar manualmente una entrega puntual',
|
||||
security: [['oauth2' => ['account:write']]],
|
||||
tags: ['Account'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'client', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'webhook', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'delivery', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Reintento encolado'),
|
||||
new OA\Response(response: 404, description: 'No es tuyo o no existe'),
|
||||
]
|
||||
)]
|
||||
public function retryDelivery(Request $request, Client $client, WebhookEndpoint $webhook, WebhookDelivery $delivery)
|
||||
{
|
||||
abort_unless($client->owner_id === $this->requireUser($request)->id, 404);
|
||||
abort_unless($webhook->client_id === $client->id, 404);
|
||||
abort_unless($delivery->webhook_endpoint_id === $webhook->id, 404);
|
||||
|
||||
SendWebhookJob::dispatch($delivery);
|
||||
|
||||
return response()->json(['queued' => true]);
|
||||
}
|
||||
|
||||
private function requireUser(Request $request): User
|
||||
{
|
||||
return $request->user('api') ?? abort(422, __('messages.self_service_requires_user'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user