Create one and see its secret once, send a test, change the secret, and read each delivery with what the other end answered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
3.9 KiB
PHP
135 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Api\V1;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\Api\V1\WebhookRequest;
|
|
use App\Http\Resources\WebhookDeliveryResource;
|
|
use App\Http\Resources\WebhookResource;
|
|
use App\Models\Webhook;
|
|
use App\Models\WebhookDelivery;
|
|
use App\Support\Webhooks\Webhooks;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Outgoing webhooks: where to send, which events, and how it has gone.
|
|
*/
|
|
class WebhookController extends Controller
|
|
{
|
|
public function index(Request $request): AnonymousResourceCollection
|
|
{
|
|
$this->authorize('viewAny', Webhook::class);
|
|
|
|
return WebhookResource::collection(
|
|
Webhook::query()->latest('id')->paginate(perPage: min($request->integer('per_page', 25), 100)),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The secret is in the answer once, when it is made (or rotated), and
|
|
* never again.
|
|
*/
|
|
public function store(WebhookRequest $request): JsonResponse
|
|
{
|
|
$this->authorize('create', Webhook::class);
|
|
|
|
$secret = $request->input('secret') ?: Str::random(40);
|
|
|
|
$webhook = new Webhook([...$request->validated(), 'secret' => $secret]);
|
|
$webhook->forceFill(['created_by' => $request->user()->id])->save();
|
|
|
|
return WebhookResource::make($webhook->refresh())
|
|
->additional(['secret' => $secret])
|
|
->response()
|
|
->setStatusCode(201);
|
|
}
|
|
|
|
public function show(Webhook $webhook): WebhookResource
|
|
{
|
|
$this->authorize('view', $webhook);
|
|
|
|
return WebhookResource::make($webhook);
|
|
}
|
|
|
|
/**
|
|
* Switching it back on starts its failure count again.
|
|
*/
|
|
public function update(WebhookRequest $request, Webhook $webhook): JsonResponse
|
|
{
|
|
$this->authorize('update', $webhook);
|
|
|
|
$data = $request->validated();
|
|
$rotated = $request->boolean('rotate_secret') ? Str::random(40) : null;
|
|
unset($data['rotate_secret']);
|
|
|
|
$webhook->fill($data);
|
|
|
|
if ($rotated !== null) {
|
|
$webhook->secret = $rotated;
|
|
}
|
|
|
|
if ($webhook->isDirty('is_active') && $webhook->is_active) {
|
|
$webhook->consecutive_failures = 0;
|
|
}
|
|
|
|
$webhook->save();
|
|
|
|
return WebhookResource::make($webhook)
|
|
->additional($rotated !== null ? ['secret' => $rotated] : [])
|
|
->response();
|
|
}
|
|
|
|
public function destroy(Webhook $webhook): JsonResponse
|
|
{
|
|
$this->authorize('delete', $webhook);
|
|
|
|
$webhook->delete();
|
|
|
|
return response()->json(status: 204);
|
|
}
|
|
|
|
/**
|
|
* Newest first. Filter: status.
|
|
*/
|
|
public function deliveries(Request $request, Webhook $webhook): AnonymousResourceCollection
|
|
{
|
|
$this->authorize('view', $webhook);
|
|
|
|
return WebhookDeliveryResource::collection(
|
|
$webhook->deliveries()
|
|
->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
|
|
->paginate(perPage: min($request->integer('per_page', 25), 100))
|
|
->withQueryString(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A `ping`, to check the receiving end before anything real happens.
|
|
*/
|
|
public function ping(Webhook $webhook): JsonResponse
|
|
{
|
|
$this->authorize('update', $webhook);
|
|
|
|
$delivery = Webhooks::ping($webhook);
|
|
|
|
return WebhookDeliveryResource::make($delivery)->response()->setStatusCode(202);
|
|
}
|
|
|
|
/**
|
|
* Send one again, as a new delivery of the same event and payload.
|
|
*/
|
|
public function redeliver(WebhookDelivery $delivery): JsonResponse
|
|
{
|
|
$this->authorize('update', $delivery->webhook);
|
|
|
|
$again = Webhooks::redeliver($delivery);
|
|
|
|
return WebhookDeliveryResource::make($again)->response()->setStatusCode(202);
|
|
}
|
|
}
|