Send signed outgoing webhooks
Fed by the records themselves, delivered only to public HTTPS addresses checked at every attempt, retried, and switched off after repeated failures. A submitter's details travel only to webhooks that opted into them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
80
app/Models/Webhook.php
Normal file
80
app/Models/Webhook.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Somewhere to tell when something happens here.
|
||||
*/
|
||||
#[Fillable(['name', 'url', 'secret', 'events', 'include_personal_data', 'is_active'])]
|
||||
#[Hidden(['secret'])]
|
||||
class Webhook extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* What can be subscribed to. `*` is all of them.
|
||||
*/
|
||||
public const EVENTS = [
|
||||
'application.received',
|
||||
'content.published',
|
||||
'content.unpublished',
|
||||
'review.requested',
|
||||
'review.returned',
|
||||
];
|
||||
|
||||
/**
|
||||
* Switched off after this many failed deliveries in a row, so a dead
|
||||
* endpoint does not keep a queue busy forever. Switching it back on is a
|
||||
* person's decision.
|
||||
*/
|
||||
public const MAX_FAILURES = 20;
|
||||
|
||||
protected $attributes = [
|
||||
'include_personal_data' => false,
|
||||
'is_active' => true,
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'secret' => 'encrypted',
|
||||
'events' => 'array',
|
||||
'include_personal_data' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'last_delivered_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<WebhookDelivery, $this>
|
||||
*/
|
||||
public function deliveries(): HasMany
|
||||
{
|
||||
return $this->hasMany(WebhookDelivery::class)->latest('id');
|
||||
}
|
||||
|
||||
public function listensTo(string $event): bool
|
||||
{
|
||||
$events = $this->events ?? [];
|
||||
|
||||
return in_array('*', $events, true) || in_array($event, $events, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<$this> $query
|
||||
* @return Builder<$this>
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
}
|
||||
40
app/Models/WebhookDelivery.php
Normal file
40
app/Models/WebhookDelivery.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* One attempt to tell a webhook about one event, and how it went.
|
||||
*/
|
||||
#[Fillable(['uuid', 'event', 'payload', 'status', 'attempts', 'response_status', 'error', 'delivered_at'])]
|
||||
class WebhookDelivery extends Model
|
||||
{
|
||||
public const PENDING = 'pending';
|
||||
|
||||
public const SUCCEEDED = 'succeeded';
|
||||
|
||||
public const FAILED = 'failed';
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'payload' => 'array',
|
||||
'attempts' => 'integer',
|
||||
'response_status' => 'integer',
|
||||
'delivered_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Webhook, $this>
|
||||
*/
|
||||
public function webhook(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Webhook::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user