Skip to content

feat: Webhooks #165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d869426
chore: install Spatie webhooks server
danjohnson95 Jan 3, 2025
edbd3c4
chore: create migration for subscriptions table
danjohnson95 Jan 3, 2025
2b15b07
chore: create enum for all events
danjohnson95 Jan 3, 2025
520b476
chore: create model for subscription
danjohnson95 Jan 3, 2025
eef30dc
chore: create WebhookAttempt
danjohnson95 Jan 3, 2025
d74a78f
chore: add relation from webhook subscription to attempts
danjohnson95 Jan 3, 2025
9d6507a
feat: add config variable for when webhook logs are pruned
danjohnson95 Jan 3, 2025
1db4ab3
feat: create the SendsWebhook concern
danjohnson95 Jan 3, 2025
6ed8f7c
feat: create class for dispatching webhooks
danjohnson95 Jan 3, 2025
d322dbb
feat: include event name in webhook payload
danjohnson95 Jan 3, 2025
9d467a0
feat: create listener to dispatch webhook for events that use SendsWe…
danjohnson95 Jan 3, 2025
285a055
chore: register event for sending webhooks
danjohnson95 Jan 3, 2025
c0291c1
feat: create and register listener for creating a Webhook Attempt
danjohnson95 Jan 3, 2025
31d1788
feat: create views for managing webhooks
danjohnson95 Jan 3, 2025
f45016d
chore: add language strings for webhooks
danjohnson95 Jan 3, 2025
1c8c925
chore: remove unused strings
danjohnson95 Jan 3, 2025
c28ea88
feat: hook up events with webhooks
danjohnson95 Jan 3, 2025
bf1aec0
chore: create factories
danjohnson95 Jan 3, 2025
7935dbd
test: create test for webhook subscription model
danjohnson95 Jan 3, 2025
8e3de1e
test: create test for SendWebhookListener
danjohnson95 Jan 3, 2025
49f9239
chore: make secret hidden
danjohnson95 Jan 3, 2025
575591d
feat: include success rate
danjohnson95 Jan 4, 2025
bfd6ec4
chore: phpstan fixes
danjohnson95 Jan 4, 2025
d639e30
feat: add ability to confgure webhook queue connection and name
danjohnson95 Jan 6, 2025
ef027a9
feat: wrap helper text in link
danjohnson95 Jan 6, 2025
bceb6e4
refact: create TextWithLink Htmlable class
danjohnson95 Jan 6, 2025
e6ecbb6
chore: remove unused import
danjohnson95 Jan 6, 2025
15e054b
chore: full width settings for editing webhooks
danjohnson95 Jan 7, 2025
4f6da89
fix: satisfy phpstan with new self in place of new static
danjohnson95 Jan 7, 2025
23f2f82
feat: use full width on creation, then split for edit
danjohnson95 Jan 7, 2025
199cedb
feat: put attempts underneath buttons
danjohnson95 Jan 7, 2025
449b358
feat: support updating secret from action button
danjohnson95 Jan 7, 2025
2b29ad2
Fix tests
jbrooksuk Jan 7, 2025
1377795
Make the secret a revealable password field
jbrooksuk Jan 8, 2025
cf2d85b
fix: use correct Action class
danjohnson95 Jan 9, 2025
e99e3a2
Merge branch '106-webhooks' of github.com:danjohnson95/cachet-core in…
danjohnson95 Jan 9, 2025
a08dbea
Set user agent for webhooks
jbrooksuk Jan 9, 2025
909fb2d
Merge branch 'main' of https://github.com/cachethq/core into 106-webh…
danjohnson95 Jan 9, 2025
31681a0
Merge branch '106-webhooks' of github.com:danjohnson95/cachet-core in…
danjohnson95 Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"spatie/laravel-data": "^4.11",
"spatie/laravel-query-builder": "^5.5",
"spatie/laravel-settings": "^3.2",
"spatie/laravel-webhook-server": "^3.8",
"timacdonald/json-api": "^1.0.0-beta.4",
"twig/twig": "^3.0"
},
Expand Down
17 changes: 17 additions & 0 deletions config/cachet.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,21 @@
|
*/
'docker' => env('CACHET_DOCKER', false),

/*
|--------------------------------------------------------------------------
| Cachet Webhooks
|--------------------------------------------------------------------------
|
| Configure how Cachet sends webhooks for events.
|
*/
'webhooks' => [
'queue_connection' => env('CACHET_WEBHOOK_QUEUE_CONNECTION', 'default'),
'queue_name' => env('CACHET_WEBHOOK_QUEUE_NAME', 'webhooks'),

'logs' => [
'prune_logs_after_days' => 30,
],
],
];
33 changes: 33 additions & 0 deletions database/factories/WebhookAttemptFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Cachet\Database\Factories;

use Cachet\Enums\WebhookEventEnum;
use Cachet\Models\WebhookAttempt;
use Cachet\Models\WebhookSubscription;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Cachet\Models\WebhookAttempt>
*/
class WebhookAttemptFactory extends Factory
{
protected $model = WebhookAttempt::class;

/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'subscription_id' => WebhookSubscription::factory(),
'event' => WebhookEventEnum::component_created,
'attempt' => 0,
'payload' => [],
'response_code' => 200,
'transfer_time' => 0.1,
];
}
}
51 changes: 51 additions & 0 deletions database/factories/WebhookSubscriptionFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Cachet\Database\Factories;

use Cachet\Models\WebhookSubscription;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Cachet\Models\WebhookSubscription>
*/
class WebhookSubscriptionFactory extends Factory
{
protected $model = WebhookSubscription::class;

/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'url' => fake()->url,
'secret' => fake()->randomAscii(),
'description' => fake()->sentence(),
'send_all_events' => false,
'selected_events' => [],
];
}

/**
* Create a webhook subscription that is enabled for all events
*/
public function allEvents(): self
{
return $this->state([
'send_all_events' => true,
]);
}

/**
* Create a webhook subscription that is only enabled
* for the given events.
*/
public function selectedEvents(array $events): self
{
return $this->state([
'selected_events' => $events,
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('webhook_subscriptions', function (Blueprint $table) {
$table->id();
$table->string('url');
$table->string('secret');
$table->string('description')->nullable();
$table->boolean('send_all_events')->default(true);
$table->json('selected_events')->nullable();
$table->decimal('success_rate_24h', 5, 2)->nullable();

$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('webhook_subscriptions');
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use Cachet\Models\WebhookSubscription;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('webhook_attempts', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(WebhookSubscription::class, 'subscription_id')->onDelete('cascade');
$table->string('event');
$table->unsignedTinyInteger('attempt');
$table->json('payload');
$table->unsignedSmallInteger('response_code')->nullable();
$table->unsignedTinyInteger('transfer_time')->nullable();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::drop('webhook_attempts');
}
};
1 change: 1 addition & 0 deletions resources/lang/en/navigation.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
'manage_cachet' => 'Manage Cachet',
'manage_customization' => 'Manage Customization',
'manage_theme' => 'Manage Theme',
'manage_webhooks' => 'Manage Webhooks',
],
],

Expand Down
30 changes: 30 additions & 0 deletions resources/lang/en/webhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

return [
'resource_label' => 'Webhook|Webhooks',
'event_selection' => [
'all' => 'Send all events',
'selected' => 'Only send selected events',
],
'form' => [
'url_label' => 'Payload URL',
'url_helper' => 'Events will POST to this URL.',
'secret_label' => 'Secret',
'secret_helper' => 'The payload will be signed with this secret. See *webhook documentation* for more information.',
'description_label' => 'Description',
'event_selection_label' => 'Send all events?',
'events_label' => 'Events',
'edit_secret_label' => 'Edit secret',
'update_secret_label' => 'Update secret',
],
'attempts' => [
'heading' => 'Attempts',
'empty_state' => 'No attempts have been made to this webhook yet',
],
'list' => [
'headers' => [
'url' => 'Payload URL',
'success_rate_24h' => 'Success rate (24h)',
],
],
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<x-filament-panels::page>
<x-filament-panels::form wire:submit="save">
{{ $this->form }}

<x-filament-panels::form.actions
:actions="$this->getCachedFormActions()"
:full-width="$this->hasFullWidthFormActions()"
/>
</x-filament-panels::form>

{{ view('cachet::filament.widgets.webhook-attempts', [
'attempts' => $this->record->attempts,
]) }}
</x-filament-panels::page>
24 changes: 24 additions & 0 deletions resources/views/filament/widgets/webhook-attempts.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<x-filament::section heading="{{ __('cachet::webhook.attempts.heading') }}">
<div class="space-y-4 text-sm">
@forelse($attempts as $attempt)
<div class="flex items-center space-x-2">
<div @class([
'bg-red-100 text-red-500' => !$attempt->isSuccess(),
'bg-primary-100 text-primary-500' => $attempt->isSuccess(),
'flex-shrink-0 whitespace-nowrap px-1 py-1 rounded-md font-semibold' => true,
])>
@if ($attempt->isSuccess())
<x-heroicon-m-check class="size-5" />
@else
<x-heroicon-m-x-mark class="size-5" />
@endif
</div>

<div class="font-mono font-medium flex-1">{{ $attempt->event }}</div>
<div class="text-gray-500 flex-shrink-0">{{ $attempt->created_at?->toDateTimeString() }}</div>
</div>
@empty
<div class="text-gray-500">{{ __('cachet::webhook.attempts.empty_state') }}</div>
@endforelse
</div>
</x-filament::section>
33 changes: 33 additions & 0 deletions src/Actions/Webhook/DispatchWebhooks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Cachet\Actions\Webhook;

use Cachet\Enums\WebhookEventEnum;
use Cachet\Models\WebhookSubscription;
use Illuminate\Database\Eloquent\Collection;

class DispatchWebhooks
{
protected mixed $event;
protected WebhookEventEnum $eventName;

public function handle(mixed $event): void
{
$this->event = $event;
$this->eventName = $this->event->getWebhookEventName();

$payload = $this->event->getWebhookPayload();

foreach ($this->getWebhookSubscriptionsForEvent() as $webhookSubscription) {
$webhookSubscription->makeCall($this->eventName, $payload)->dispatch();
}
}

/**
* @return Collection<WebhookSubscription>
*/
private function getWebhookSubscriptionsForEvent(): Collection
{
return WebhookSubscription::whereEvent($this->eventName)->get();
}
}
5 changes: 5 additions & 0 deletions src/Cachet.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class Cachet
*/
public const USER_AGENT = 'Cachet/3.0 (+https://docs.cachethq.io)';

/**
* The user agent used by Cachet's webhooks.
*/
public const WEBHOOK_USER_AGENT = 'Cachet/3.0 Webhook (+https://docs.cachethq.io)';

/**
* Get the current user using `cachet.guard`.
*/
Expand Down
8 changes: 8 additions & 0 deletions src/CachetCoreServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Cachet;

use BladeUI\Icons\Factory;
use Cachet\Listeners\SendWebhookListener;
use Cachet\Listeners\WebhookCallEventListener;
use Cachet\Models\Incident;
use Cachet\Models\Schedule;
use Cachet\Settings\AppSettings;
Expand All @@ -14,10 +16,13 @@
use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Spatie\WebhookServer\Events\WebhookCallFailedEvent;
use Spatie\WebhookServer\Events\WebhookCallSucceededEvent;

class CachetCoreServiceProvider extends ServiceProvider
{
Expand Down Expand Up @@ -56,6 +61,9 @@ public function boot(): void
$this->registerPublishing();
$this->registerBladeComponents();

Event::listen('Cachet\Events\Incidents\*', SendWebhookListener::class);
Event::listen([WebhookCallSucceededEvent::class, WebhookCallFailedEvent::class], WebhookCallEventListener::class);

Http::globalRequestMiddleware(fn ($request) => $request->withHeader(
'User-Agent', Cachet::USER_AGENT
));
Expand Down
19 changes: 19 additions & 0 deletions src/Concerns/SendsWebhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Cachet\Concerns;

use Cachet\Enums\WebhookEventEnum;
use Exception;

trait SendsWebhook
{
public function getWebhookPayload(): array
{
return [];
}

public function getWebhookEventName(): WebhookEventEnum
{
throw new Exception('You must implement the getWebhookEventName method on ' . static::class);
}
}
25 changes: 25 additions & 0 deletions src/Enums/WebhookEventEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Cachet\Enums;

enum WebhookEventEnum: string
{
case component_created = 'component_created';
case component_updated = 'component_updated';
case component_deleted = 'component_deleted';
case component_status_changed = 'component_status_changed';

case incident_created = 'incident_created';
case incident_updated = 'incident_updated';
case incident_deleted = 'incident_deleted';

case metric_created = 'metric_created';
case metric_updated = 'metric_updated';
case metric_deleted = 'metric_deleted';
case metric_point_created = 'metric_point_created';
case metric_point_deleted = 'metric_point_deleted';

case subscriber_created = 'subscriber_created';
case subscriber_unsubscribed = 'subscriber_unsubscribed';
case subscriber_verified = 'subscriber_verified';
}
Loading
Loading