mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-20 05:14:46 +02:00
Implement Webhooks (#548)
* feat: First Webhook PoC draft * feat: Dispatch Webhooks PoC * fix: typo in webhook configuration scope * Update 2024_04_21_162552_create_webhooks_table.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update 2024_04_21_162552_create_webhooks_table.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update 2024_04_21_162544_create_webhook_configurations_table.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update 2024_04_21_162544_create_webhook_configurations_table.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update DispatchWebhooks.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update DispatchWebhooksJob.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update DispatchWebhookForConfiguration.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update DispatchWebhookForConfiguration.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update DispatchWebhookForConfiguration.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update DispatchWebhooksJob.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update DispatchWebhooksJob.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * Update DispatchWebhooksJob.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * chore: Implement Webhook Event Discovery * we got a test working for webhooks * WIP * Something is working! * More tests * clean up the tests now that they are passing * WIP * Don't use model specific events * WIP * WIP * WIP * WIP * WIP * Do it sync * Reset these * Don't need restored event type * Deleted some unused jobs * Find custom Events * Remove observers * Add custom event test * Run Pint * Add caching * Don't cache every single event * Fix tests * Run Pint * Phpstan fixes * Pint fix * Test fixes * Middleware unit test fix * Pint fixes * Remove index not working for older dbs * Use facade instead --------- Co-authored-by: Pascale Beier <mail@pascalebeier.de> Co-authored-by: Lance Pioch <lancepioch@gmail.com> Co-authored-by: Vehikl <go@vehikl.com>
This commit is contained in:
parent
5f77deb1fd
commit
86c369d7ce
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Server;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Created extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Server $server)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Server;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Creating extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Server $server)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Server;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Deleted extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Server $server)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Server;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Deleting extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Server $server)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Server;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Saved extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Server $server)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Server;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Saving extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Server $server)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Server;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Updated extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Server $server)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Server;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Updating extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Server $server)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Subuser;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Subuser;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Created extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Subuser $subuser)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Subuser;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Subuser;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Creating extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Subuser $subuser)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Subuser;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Subuser;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Deleted extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Subuser $subuser)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\Subuser;
|
|
||||||
|
|
||||||
use App\Events\Event;
|
|
||||||
use App\Models\Subuser;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Deleting extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public Subuser $subuser)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\User;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Events\Event;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Created extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\User;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Events\Event;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Creating extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\User;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Events\Event;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Deleted extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Events\User;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Events\Event;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class Deleting extends Event
|
|
||||||
{
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new event instance.
|
|
||||||
*/
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
71
app/Filament/Resources/WebhookResource.php
Normal file
71
app/Filament/Resources/WebhookResource.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WebhookResource\Pages;
|
||||||
|
use App\Models\WebhookConfiguration;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class WebhookResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = WebhookConfiguration::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'tabler-webhook';
|
||||||
|
|
||||||
|
protected static ?string $label = 'Webhooks';
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('endpoint')->activeUrl()->required(),
|
||||||
|
Forms\Components\TextInput::make('description')->nullable(),
|
||||||
|
Forms\Components\CheckboxList::make('events')->lazy()->options(
|
||||||
|
fn () => WebhookConfiguration::filamentCheckboxList()
|
||||||
|
)
|
||||||
|
->columns(3)
|
||||||
|
->columnSpanFull()
|
||||||
|
->gridDirection('row')
|
||||||
|
->required(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListWebhookConfigurations::route('/'),
|
||||||
|
'create' => Pages\CreateWebhookConfiguration::route('/create'),
|
||||||
|
'edit' => Pages\EditWebhookConfiguration::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WebhookResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WebhookResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateWebhookConfiguration extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = WebhookResource::class;
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WebhookResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WebhookResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditWebhookConfiguration extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = WebhookResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WebhookResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WebhookResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListWebhookConfigurations extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = WebhookResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Api\Client\Servers;
|
namespace App\Http\Controllers\Api\Client\Servers;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Notifications\RemovedFromServer;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@ -144,6 +145,11 @@ class SubuserController extends ClientApiController
|
|||||||
$log->transaction(function ($instance) use ($server, $subuser) {
|
$log->transaction(function ($instance) use ($server, $subuser) {
|
||||||
$subuser->delete();
|
$subuser->delete();
|
||||||
|
|
||||||
|
$subuser->user->notify(new RemovedFromServer([
|
||||||
|
'user' => $subuser->user->name_first,
|
||||||
|
'name' => $subuser->server->name,
|
||||||
|
]));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
|
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
|
||||||
} catch (DaemonConnectionException $exception) {
|
} catch (DaemonConnectionException $exception) {
|
||||||
|
40
app/Jobs/ProcessWebhook.php
Normal file
40
app/Jobs/ProcessWebhook.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\WebhookConfiguration;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class ProcessWebhook implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private WebhookConfiguration $webhookConfiguration,
|
||||||
|
private string $eventName,
|
||||||
|
private array $data
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Http::post($this->webhookConfiguration->endpoint, $this->data)->throw();
|
||||||
|
$successful = now();
|
||||||
|
} catch (\Exception) {
|
||||||
|
$successful = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->webhookConfiguration->webhooks()->create([
|
||||||
|
'payload' => $this->data,
|
||||||
|
'successful_at' => $successful,
|
||||||
|
'event' => $this->eventName,
|
||||||
|
'endpoint' => $this->webhookConfiguration->endpoint,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
39
app/Listeners/DispatchWebhooks.php
Normal file
39
app/Listeners/DispatchWebhooks.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Jobs\ProcessWebhook;
|
||||||
|
use App\Models\WebhookConfiguration;
|
||||||
|
|
||||||
|
class DispatchWebhooks
|
||||||
|
{
|
||||||
|
public function handle(string $eventName, array $data): void
|
||||||
|
{
|
||||||
|
if (!$this->eventIsWatched($eventName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$matchingHooks = cache()->rememberForever("webhooks.$eventName", function () use ($eventName) {
|
||||||
|
return WebhookConfiguration::query()->whereJsonContains('events', $eventName)->get();
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($matchingHooks ?? [] as $webhookConfig) {
|
||||||
|
if (in_array($eventName, $webhookConfig->events)) {
|
||||||
|
ProcessWebhook::dispatch($webhookConfig, $eventName, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function eventIsWatched(string $eventName): bool
|
||||||
|
{
|
||||||
|
$watchedEvents = cache()->rememberForever('watchedWebhooks', function () {
|
||||||
|
return WebhookConfiguration::pluck('events')
|
||||||
|
->flatten()
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
});
|
||||||
|
|
||||||
|
return in_array($eventName, $watchedEvents);
|
||||||
|
}
|
||||||
|
}
|
21
app/Models/Webhook.php
Normal file
21
app/Models/Webhook.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Webhook extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['payload', 'successful_at', 'event', 'endpoint'];
|
||||||
|
|
||||||
|
public function casts()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'payload' => 'array',
|
||||||
|
'successful_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
120
app/Models/WebhookConfiguration.php
Normal file
120
app/Models/WebhookConfiguration.php
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
|
class WebhookConfiguration extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'endpoint',
|
||||||
|
'description',
|
||||||
|
'events',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'events' => 'json',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
self::saved(static function (self $webhookConfiguration): void {
|
||||||
|
$changedEvents = collect([
|
||||||
|
...((array) $webhookConfiguration->events),
|
||||||
|
...$webhookConfiguration->getOriginal('events', '[]'),
|
||||||
|
])->unique();
|
||||||
|
|
||||||
|
$changedEvents->each(function (string $event) {
|
||||||
|
cache()->forever("webhooks.$event", WebhookConfiguration::query()->whereJsonContains('events', $event)->get());
|
||||||
|
});
|
||||||
|
|
||||||
|
cache()->forever('watchedWebhooks', WebhookConfiguration::pluck('events')->flatten()->unique()->values()->all());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function webhooks(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Webhook::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function allPossibleEvents(): array
|
||||||
|
{
|
||||||
|
return static::discoverCustomEvents() + static::allModelEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function filamentCheckboxList(): array
|
||||||
|
{
|
||||||
|
$list = [];
|
||||||
|
$events = static::allPossibleEvents();
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$list[$event] = static::transformClassName($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function transformClassName(string $event): string
|
||||||
|
{
|
||||||
|
return str($event)
|
||||||
|
->after('eloquent.')
|
||||||
|
->replace('App\\Models\\', '')
|
||||||
|
->replace('App\\Events\\', 'event: ')
|
||||||
|
->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function allModelEvents(): array
|
||||||
|
{
|
||||||
|
$eventTypes = ['created', 'updated', 'deleted'];
|
||||||
|
$models = static::discoverModels();
|
||||||
|
|
||||||
|
$events = [];
|
||||||
|
foreach ($models as $model) {
|
||||||
|
foreach ($eventTypes as $eventType) {
|
||||||
|
$events[] = "eloquent.$eventType: $model";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function discoverModels(): array
|
||||||
|
{
|
||||||
|
$namespace = 'App\\Models\\';
|
||||||
|
$directory = app_path('Models');
|
||||||
|
|
||||||
|
$models = [];
|
||||||
|
foreach (File::allFiles($directory) as $file) {
|
||||||
|
$models[] = $namespace . str($file->getFilename())
|
||||||
|
->replace([DIRECTORY_SEPARATOR, '.php'], ['\\', '']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $models;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function discoverCustomEvents(): array
|
||||||
|
{
|
||||||
|
$directory = app_path('Events');
|
||||||
|
|
||||||
|
$events = [];
|
||||||
|
foreach (File::allFiles($directory) as $file) {
|
||||||
|
$namespace = str($file->getPath())
|
||||||
|
->after(base_path())
|
||||||
|
->replace(DIRECTORY_SEPARATOR, '\\')
|
||||||
|
->replace('\\app\\', 'App\\')
|
||||||
|
->toString();
|
||||||
|
|
||||||
|
$events[] = $namespace . '\\' . str($file->getFilename())
|
||||||
|
->replace([DIRECTORY_SEPARATOR, '.php'], ['\\', '']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Observers;
|
|
||||||
|
|
||||||
use App\Models\EggVariable;
|
|
||||||
|
|
||||||
class EggVariableObserver
|
|
||||||
{
|
|
||||||
public function creating(EggVariable $variable): void
|
|
||||||
{
|
|
||||||
if (isset($variable->field_type)) {
|
|
||||||
unset($variable->field_type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updating(EggVariable $variable): void
|
|
||||||
{
|
|
||||||
if (isset($variable->field_type)) {
|
|
||||||
unset($variable->field_type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Observers;
|
|
||||||
|
|
||||||
use App\Events;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
|
||||||
|
|
||||||
class ServerObserver
|
|
||||||
{
|
|
||||||
use DispatchesJobs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Server creating event.
|
|
||||||
*/
|
|
||||||
public function creating(Server $server): void
|
|
||||||
{
|
|
||||||
event(new Events\Server\Creating($server));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Server created event.
|
|
||||||
*/
|
|
||||||
public function created(Server $server): void
|
|
||||||
{
|
|
||||||
event(new Events\Server\Created($server));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Server deleting event.
|
|
||||||
*/
|
|
||||||
public function deleting(Server $server): void
|
|
||||||
{
|
|
||||||
event(new Events\Server\Deleting($server));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Server deleted event.
|
|
||||||
*/
|
|
||||||
public function deleted(Server $server): void
|
|
||||||
{
|
|
||||||
event(new Events\Server\Deleted($server));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Server saving event.
|
|
||||||
*/
|
|
||||||
public function saving(Server $server): void
|
|
||||||
{
|
|
||||||
event(new Events\Server\Saving($server));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Server saved event.
|
|
||||||
*/
|
|
||||||
public function saved(Server $server): void
|
|
||||||
{
|
|
||||||
event(new Events\Server\Saved($server));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Server updating event.
|
|
||||||
*/
|
|
||||||
public function updating(Server $server): void
|
|
||||||
{
|
|
||||||
event(new Events\Server\Updating($server));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Server saved event.
|
|
||||||
*/
|
|
||||||
public function updated(Server $server): void
|
|
||||||
{
|
|
||||||
event(new Events\Server\Updated($server));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Observers;
|
|
||||||
|
|
||||||
use App\Events;
|
|
||||||
use App\Models\Subuser;
|
|
||||||
use App\Notifications\AddedToServer;
|
|
||||||
use App\Notifications\RemovedFromServer;
|
|
||||||
|
|
||||||
class SubuserObserver
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Listen to the Subuser creating event.
|
|
||||||
*/
|
|
||||||
public function creating(Subuser $subuser): void
|
|
||||||
{
|
|
||||||
event(new Events\Subuser\Creating($subuser));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Subuser created event.
|
|
||||||
*/
|
|
||||||
public function created(Subuser $subuser): void
|
|
||||||
{
|
|
||||||
event(new Events\Subuser\Created($subuser));
|
|
||||||
|
|
||||||
$subuser->user->notify(new AddedToServer([
|
|
||||||
'user' => $subuser->user->name_first,
|
|
||||||
'name' => $subuser->server->name,
|
|
||||||
'uuid_short' => $subuser->server->uuid_short,
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Subuser deleting event.
|
|
||||||
*/
|
|
||||||
public function deleting(Subuser $subuser): void
|
|
||||||
{
|
|
||||||
event(new Events\Subuser\Deleting($subuser));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the Subuser deleted event.
|
|
||||||
*/
|
|
||||||
public function deleted(Subuser $subuser): void
|
|
||||||
{
|
|
||||||
event(new Events\Subuser\Deleted($subuser));
|
|
||||||
|
|
||||||
$subuser->user->notify(new RemovedFromServer([
|
|
||||||
'user' => $subuser->user->name_first,
|
|
||||||
'name' => $subuser->server->name,
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Observers;
|
|
||||||
|
|
||||||
use App\Events;
|
|
||||||
use App\Models\User;
|
|
||||||
|
|
||||||
class UserObserver
|
|
||||||
{
|
|
||||||
protected string $uuid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the User creating event.
|
|
||||||
*/
|
|
||||||
public function creating(User $user): void
|
|
||||||
{
|
|
||||||
event(new Events\User\Creating($user));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the User created event.
|
|
||||||
*/
|
|
||||||
public function created(User $user): void
|
|
||||||
{
|
|
||||||
event(new Events\User\Created($user));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the User deleting event.
|
|
||||||
*/
|
|
||||||
public function deleting(User $user): void
|
|
||||||
{
|
|
||||||
event(new Events\User\Deleting($user));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to the User deleted event.
|
|
||||||
*/
|
|
||||||
public function deleted(User $user): void
|
|
||||||
{
|
|
||||||
event(new Events\User\Deleted($user));
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,16 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Listeners\DispatchWebhooks;
|
||||||
use App\Models\Server;
|
|
||||||
use App\Models\Subuser;
|
|
||||||
use App\Models\EggVariable;
|
|
||||||
use App\Observers\UserObserver;
|
|
||||||
use App\Observers\ServerObserver;
|
|
||||||
use App\Observers\SubuserObserver;
|
|
||||||
use App\Observers\EggVariableObserver;
|
|
||||||
use App\Events\Server\Installed as ServerInstalledEvent;
|
|
||||||
use App\Notifications\ServerInstalled as ServerInstalledNotification;
|
|
||||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||||
|
|
||||||
class EventServiceProvider extends ServiceProvider
|
class EventServiceProvider extends ServiceProvider
|
||||||
@ -20,19 +11,9 @@ class EventServiceProvider extends ServiceProvider
|
|||||||
* The event to listener mappings for the application.
|
* The event to listener mappings for the application.
|
||||||
*/
|
*/
|
||||||
protected $listen = [
|
protected $listen = [
|
||||||
ServerInstalledEvent::class => [ServerInstalledNotification::class],
|
'App\\*' => [DispatchWebhooks::class],
|
||||||
|
'eloquent.created*' => [DispatchWebhooks::class],
|
||||||
|
'eloquent.deleted*' => [DispatchWebhooks::class],
|
||||||
|
'eloquent.updated*' => [DispatchWebhooks::class],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* Register any events for your application.
|
|
||||||
*/
|
|
||||||
public function boot(): void
|
|
||||||
{
|
|
||||||
parent::boot();
|
|
||||||
|
|
||||||
User::observe(UserObserver::class);
|
|
||||||
Server::observe(ServerObserver::class);
|
|
||||||
Subuser::observe(SubuserObserver::class);
|
|
||||||
EggVariable::observe(EggVariableObserver::class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -48,8 +48,7 @@ class EggExporterService
|
|||||||
],
|
],
|
||||||
'variables' => $egg->variables->map(function (EggVariable $eggVariable) {
|
'variables' => $egg->variables->map(function (EggVariable $eggVariable) {
|
||||||
return Collection::make($eggVariable->toArray())
|
return Collection::make($eggVariable->toArray())
|
||||||
->except(['id', 'egg_id', 'created_at', 'updated_at'])
|
->except(['id', 'egg_id', 'created_at', 'updated_at']);
|
||||||
->merge(['field_type' => 'text']);
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -48,6 +48,11 @@ class EggImporterService
|
|||||||
'copy_script_from' => null,
|
'copy_script_from' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Don't check for this anymore
|
||||||
|
for ($i = 0; $i < count($parsed['variables']); $i++) {
|
||||||
|
unset($parsed['variables'][$i]['field_type']);
|
||||||
|
}
|
||||||
|
|
||||||
$egg = $this->fillFromParsed($egg, $parsed);
|
$egg = $this->fillFromParsed($egg, $parsed);
|
||||||
$egg->save();
|
$egg->save();
|
||||||
|
|
||||||
@ -157,17 +162,13 @@ class EggImporterService
|
|||||||
$images = $parsed['images'];
|
$images = $parsed['images'];
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($parsed['images'], $parsed['image']);
|
unset($parsed['images'], $parsed['image'], $parsed['field_type']);
|
||||||
|
|
||||||
$parsed['docker_images'] = [];
|
$parsed['docker_images'] = [];
|
||||||
foreach ($images as $image) {
|
foreach ($images as $image) {
|
||||||
$parsed['docker_images'][$image] = $image;
|
$parsed['docker_images'][$image] = $image;
|
||||||
}
|
}
|
||||||
|
|
||||||
$parsed['variables'] = array_map(function ($value) {
|
|
||||||
return array_merge($value, ['field_type' => 'text']);
|
|
||||||
}, $parsed['variables']);
|
|
||||||
|
|
||||||
return $parsed;
|
return $parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
namespace App\Services\Subusers;
|
namespace App\Services\Subusers;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Notifications\AddedToServer;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Subuser;
|
use App\Models\Subuser;
|
||||||
@ -59,11 +60,19 @@ class SubuserCreationService
|
|||||||
throw new ServerSubuserExistsException(trans('exceptions.subusers.subuser_exists'));
|
throw new ServerSubuserExistsException(trans('exceptions.subusers.subuser_exists'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Subuser::query()->create([
|
$subuser = Subuser::query()->create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'server_id' => $server->id,
|
'server_id' => $server->id,
|
||||||
'permissions' => array_unique($permissions),
|
'permissions' => array_unique($permissions),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$subuser->user->notify(new AddedToServer([
|
||||||
|
'user' => $subuser->user->name_first,
|
||||||
|
'name' => $subuser->server->name,
|
||||||
|
'uuid_short' => $subuser->server->uuid_short,
|
||||||
|
]));
|
||||||
|
|
||||||
|
return $subuser;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
app/Traits/Services/HasWebhookPayload.php
Normal file
15
app/Traits/Services/HasWebhookPayload.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits\Services;
|
||||||
|
|
||||||
|
trait HasWebhookPayload
|
||||||
|
{
|
||||||
|
public function getPayload(): array
|
||||||
|
{
|
||||||
|
if (method_exists($this, '__serialize')) {
|
||||||
|
return $this->__serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Node;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Allocation;
|
use App\Models\Allocation;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
@ -23,6 +24,7 @@ class AllocationFactory extends Factory
|
|||||||
return [
|
return [
|
||||||
'ip' => $this->faker->unique()->ipv4(),
|
'ip' => $this->faker->unique()->ipv4(),
|
||||||
'port' => $this->faker->unique()->numberBetween(1024, 65535),
|
'port' => $this->faker->unique()->numberBetween(1024, 65535),
|
||||||
|
'node_id' => Node::factory(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,12 @@ class EggFactory extends Factory
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'uuid' => Uuid::uuid4()->toString(),
|
'uuid' => Uuid::uuid4()->toString(),
|
||||||
|
'author' => $this->faker->email(),
|
||||||
|
'docker_images' => ['a', 'b', 'c'],
|
||||||
|
'config_logs' => '{}',
|
||||||
|
'config_startup' => '{}',
|
||||||
|
'config_stop' => '{}',
|
||||||
|
'config_files' => '{}',
|
||||||
'name' => $this->faker->name(),
|
'name' => $this->faker->name(),
|
||||||
'description' => implode(' ', $this->faker->sentences()),
|
'description' => implode(' ', $this->faker->sentences()),
|
||||||
'startup' => 'java -jar test.jar',
|
'startup' => 'java -jar test.jar',
|
||||||
|
@ -40,6 +40,7 @@ class NodeFactory extends Factory
|
|||||||
'daemon_listen' => 8080,
|
'daemon_listen' => 8080,
|
||||||
'daemon_sftp' => 2022,
|
'daemon_sftp' => 2022,
|
||||||
'daemon_base' => '/var/lib/panel/volumes',
|
'daemon_base' => '/var/lib/panel/volumes',
|
||||||
|
'maintenance_mode' => false,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Allocation;
|
||||||
|
use App\Models\Egg;
|
||||||
|
use App\Models\Node;
|
||||||
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@ -17,12 +21,28 @@ class ServerFactory extends Factory
|
|||||||
*/
|
*/
|
||||||
protected $model = Server::class;
|
protected $model = Server::class;
|
||||||
|
|
||||||
|
public function withNode(?Node $node = null): static
|
||||||
|
{
|
||||||
|
$node ??= Node::factory()->create();
|
||||||
|
|
||||||
|
return $this->state(fn () => [
|
||||||
|
'node_id' => $node->id,
|
||||||
|
'allocation_id' => Allocation::factory([
|
||||||
|
'node_id' => $node->id,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the model's default state.
|
* Define the model's default state.
|
||||||
*/
|
*/
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'owner_id' => User::factory(),
|
||||||
|
'node_id' => Node::factory(),
|
||||||
|
'allocation_id' => Allocation::factory(),
|
||||||
|
'egg_id' => Egg::factory(),
|
||||||
'uuid' => Uuid::uuid4()->toString(),
|
'uuid' => Uuid::uuid4()->toString(),
|
||||||
'uuid_short' => Str::lower(Str::random(8)),
|
'uuid_short' => Str::lower(Str::random(8)),
|
||||||
'name' => $this->faker->firstName(),
|
'name' => $this->faker->firstName(),
|
||||||
|
20
database/Factories/WebhookConfigurationFactory.php
Normal file
20
database/Factories/WebhookConfigurationFactory.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\WebhookConfiguration;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class WebhookConfigurationFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = WebhookConfiguration::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'endpoint' => fake()->url(),
|
||||||
|
'description' => fake()->sentence(),
|
||||||
|
'events' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,6 @@ class EggSeeder extends Seeder
|
|||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
foreach (static::$imports as $import) {
|
foreach (static::$imports as $import) {
|
||||||
/* @noinspection PhpParamsInspection */
|
|
||||||
$this->parseEggFiles($import);
|
$this->parseEggFiles($import);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
<?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_configurations', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('endpoint');
|
||||||
|
$table->string('description');
|
||||||
|
$table->json('events');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('webhook_configurations');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,32 @@
|
|||||||
|
<?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('webhooks', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignIdFor(\App\Models\WebhookConfiguration::class)->constrained();
|
||||||
|
$table->string('event');
|
||||||
|
$table->string('endpoint');
|
||||||
|
$table->timestamp('successful_at')->nullable();
|
||||||
|
$table->json('payload');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('webhooks');
|
||||||
|
}
|
||||||
|
};
|
69
tests/Feature/DispatchWebhooksTest.php
Normal file
69
tests/Feature/DispatchWebhooksTest.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Feature;
|
||||||
|
|
||||||
|
use App\Jobs\ProcessWebhook;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\WebhookConfiguration;
|
||||||
|
use App\Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
|
||||||
|
class DispatchWebhooksTest extends TestCase
|
||||||
|
{
|
||||||
|
use LazilyRefreshDatabase;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Queue::fake();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_sends_a_single_webhook(): void
|
||||||
|
{
|
||||||
|
WebhookConfiguration::factory()->create([
|
||||||
|
'events' => ['eloquent.created: '.Server::class],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->createServer();
|
||||||
|
|
||||||
|
Queue::assertPushed(ProcessWebhook::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sends_multiple_webhooks()
|
||||||
|
{
|
||||||
|
WebhookConfiguration::factory(2)
|
||||||
|
->create(['events' => ['eloquent.created: '.Server::class]]);
|
||||||
|
|
||||||
|
$this->createServer();
|
||||||
|
|
||||||
|
Queue::assertPushed(ProcessWebhook::class, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_sends_no_webhooks()
|
||||||
|
{
|
||||||
|
WebhookConfiguration::factory()->create();
|
||||||
|
|
||||||
|
$this->createServer();
|
||||||
|
|
||||||
|
Queue::assertNothingPushed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_sends_some_webhooks()
|
||||||
|
{
|
||||||
|
WebhookConfiguration::factory(2)
|
||||||
|
->sequence(
|
||||||
|
['events' => ['eloquent.created: '.Server::class]],
|
||||||
|
['events' => ['eloquent.deleted: '.Server::class]]
|
||||||
|
)->create();
|
||||||
|
|
||||||
|
$this->createServer();
|
||||||
|
|
||||||
|
Queue::assertPushed(ProcessWebhook::class, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createServer(): Server
|
||||||
|
{
|
||||||
|
return Server::factory()->withNode()->create();
|
||||||
|
}
|
||||||
|
}
|
204
tests/Feature/ProcessWebhooksTest.php
Normal file
204
tests/Feature/ProcessWebhooksTest.php
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Feature;
|
||||||
|
|
||||||
|
use App\Events\Server\Installed;
|
||||||
|
use App\Jobs\ProcessWebhook;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\Webhook;
|
||||||
|
use App\Models\WebhookConfiguration;
|
||||||
|
use App\Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||||
|
use Illuminate\Http\Client\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class ProcessWebhooksTest extends TestCase
|
||||||
|
{
|
||||||
|
use LazilyRefreshDatabase;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
Carbon::setTestNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_sends_a_single_webhook(): void
|
||||||
|
{
|
||||||
|
$webhook = WebhookConfiguration::factory()->create([
|
||||||
|
'events' => [$eventName = 'eloquent.created: '.Server::class],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake([$webhook->endpoint => Http::response()]);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'status' => null,
|
||||||
|
'oom_killer' => false,
|
||||||
|
'installed_at' => null,
|
||||||
|
'owner_id' => 1,
|
||||||
|
'node_id' => 1,
|
||||||
|
'allocation_id' => 1,
|
||||||
|
'egg_id' => 1,
|
||||||
|
'uuid' => '9ff9885f-ab79-4a6e-a53e-466a84cdb2d8',
|
||||||
|
'uuid_short' => 'ypk27val',
|
||||||
|
'name' => 'Delmer',
|
||||||
|
'description' => 'Est sed quibusdam sed eos quae est. Ut similique non impedit voluptas. Aperiam repellendus impedit voluptas officiis id.',
|
||||||
|
'skip_scripts' => false,
|
||||||
|
'memory' => 512,
|
||||||
|
'swap' => 0,
|
||||||
|
'disk' => 512,
|
||||||
|
'io' => 500,
|
||||||
|
'cpu' => 0,
|
||||||
|
'threads' => null,
|
||||||
|
'startup' => '/bin/bash echo "hello world"',
|
||||||
|
'image' => 'foo/bar:latest',
|
||||||
|
'allocation_limit' => null,
|
||||||
|
'database_limit' => null,
|
||||||
|
'backup_limit' => 0,
|
||||||
|
'created_at' => '2024-09-12T20:21:29.000000Z',
|
||||||
|
'updated_at' => '2024-09-12T20:21:29.000000Z',
|
||||||
|
'id' => 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
ProcessWebhook::dispatchSync(
|
||||||
|
$webhook,
|
||||||
|
'eloquent.created: '.Server::class,
|
||||||
|
$data,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertCount(1, cache()->get("webhooks.$eventName"));
|
||||||
|
$this->assertEquals($webhook->id, cache()->get("webhooks.$eventName")->first()->id);
|
||||||
|
|
||||||
|
Http::assertSentCount(1);
|
||||||
|
Http::assertSent(function (Request $request) use ($webhook, $data) {
|
||||||
|
return $webhook->endpoint === $request->url()
|
||||||
|
&& $request->data() === $data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sends_multiple_webhooks()
|
||||||
|
{
|
||||||
|
[$webhook1, $webhook2] = WebhookConfiguration::factory(2)
|
||||||
|
->create(['events' => [$eventName = 'eloquent.created: '.Server::class]]);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
$webhook1->endpoint => Http::response(),
|
||||||
|
$webhook2->endpoint => Http::response(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->createServer();
|
||||||
|
|
||||||
|
$this->assertCount(2, cache()->get("webhooks.$eventName"));
|
||||||
|
$this->assertContains($webhook1->id, cache()->get("webhooks.$eventName")->pluck('id'));
|
||||||
|
$this->assertContains($webhook2->id, cache()->get("webhooks.$eventName")->pluck('id'));
|
||||||
|
|
||||||
|
Http::assertSentCount(2);
|
||||||
|
Http::assertSent(fn (Request $request) => $webhook1->endpoint === $request->url());
|
||||||
|
Http::assertSent(fn (Request $request) => $webhook2->endpoint === $request->url());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_sends_no_webhooks()
|
||||||
|
{
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
WebhookConfiguration::factory()->create();
|
||||||
|
|
||||||
|
$this->createServer();
|
||||||
|
|
||||||
|
Http::assertSentCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_sends_some_webhooks()
|
||||||
|
{
|
||||||
|
[$webhook1, $webhook2] = WebhookConfiguration::factory(2)
|
||||||
|
->sequence(
|
||||||
|
['events' => ['eloquent.created: '.Server::class]],
|
||||||
|
['events' => ['eloquent.deleted: '.Server::class]]
|
||||||
|
)->create();
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
$webhook1->endpoint => Http::response(),
|
||||||
|
$webhook2->endpoint => Http::response(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->createServer();
|
||||||
|
|
||||||
|
Http::assertSentCount(1);
|
||||||
|
Http::assertSent(fn (Request $request) => $webhook1->endpoint === $request->url());
|
||||||
|
Http::assertNotSent(fn (Request $request) => $webhook2->endpoint === $request->url());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_records_when_a_webhook_is_sent()
|
||||||
|
{
|
||||||
|
$webhookConfig = WebhookConfiguration::factory()
|
||||||
|
->create(['events' => ['eloquent.created: '.Server::class]]);
|
||||||
|
|
||||||
|
Http::fake([$webhookConfig->endpoint => Http::response()]);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount(Webhook::class, 0);
|
||||||
|
|
||||||
|
$server = $this->createServer();
|
||||||
|
|
||||||
|
$this->assertDatabaseCount(Webhook::class, 1);
|
||||||
|
|
||||||
|
$webhook = Webhook::query()->first();
|
||||||
|
$this->assertEquals($server->uuid, $webhook->payload[0]['uuid']);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas(Webhook::class, [
|
||||||
|
'endpoint' => $webhookConfig->endpoint,
|
||||||
|
'successful_at' => now()->startOfSecond(),
|
||||||
|
'event' => 'eloquent.created: '.Server::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_records_when_a_webhook_fails()
|
||||||
|
{
|
||||||
|
$webhookConfig = WebhookConfiguration::factory()->create([
|
||||||
|
'events' => ['eloquent.created: '.Server::class],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake([$webhookConfig->endpoint => Http::response(status: 500)]);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount(Webhook::class, 0);
|
||||||
|
|
||||||
|
$server = $this->createServer();
|
||||||
|
|
||||||
|
$this->assertDatabaseCount(Webhook::class, 1);
|
||||||
|
$this->assertDatabaseHas(Webhook::class, [
|
||||||
|
'payload' => json_encode([$server->toArray()]),
|
||||||
|
'endpoint' => $webhookConfig->endpoint,
|
||||||
|
'successful_at' => null,
|
||||||
|
'event' => 'eloquent.created: '.Server::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_is_triggered_on_custom_events()
|
||||||
|
{
|
||||||
|
$webhookConfig = WebhookConfiguration::factory()->create([
|
||||||
|
'events' => [Installed::class],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake([$webhookConfig->endpoint => Http::response()]);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount(Webhook::class, 0);
|
||||||
|
|
||||||
|
$server = $this->createServer();
|
||||||
|
|
||||||
|
event(new Installed($server));
|
||||||
|
|
||||||
|
$this->assertDatabaseCount(Webhook::class, 1);
|
||||||
|
$this->assertDatabaseHas(Webhook::class, [
|
||||||
|
// 'payload' => json_encode([['server' => $server->toArray()]]),
|
||||||
|
'endpoint' => $webhookConfig->endpoint,
|
||||||
|
'successful_at' => now()->startOfSecond(),
|
||||||
|
'event' => Installed::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createServer(): Server
|
||||||
|
{
|
||||||
|
return Server::factory()->withNode()->create();
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,9 @@ abstract class TestCase extends BaseTestCase
|
|||||||
Carbon::setTestNow(Carbon::now());
|
Carbon::setTestNow(Carbon::now());
|
||||||
CarbonImmutable::setTestNow(Carbon::now());
|
CarbonImmutable::setTestNow(Carbon::now());
|
||||||
|
|
||||||
|
// TODO: if unit tests suite, then force set DB_HOST=UNIT_NO_DB
|
||||||
|
// env('DB_DATABASE', 'UNIT_NO_DB');
|
||||||
|
|
||||||
// Why, you ask? If we don't force this to false it is possible for certain exceptions
|
// Why, you ask? If we don't force this to false it is possible for certain exceptions
|
||||||
// to show their error message properly in the integration test output, but not actually
|
// to show their error message properly in the integration test output, but not actually
|
||||||
// be setup correctly to display their message in production.
|
// be setup correctly to display their message in production.
|
||||||
|
@ -29,10 +29,14 @@ class MaintenanceMiddlewareTest extends MiddlewareTestCase
|
|||||||
*/
|
*/
|
||||||
public function testHandle(): void
|
public function testHandle(): void
|
||||||
{
|
{
|
||||||
$server = Server::factory()->make();
|
// maintenance mode is off by default
|
||||||
$node = Node::factory()->make(['maintenance' => 0]);
|
$server = new Server();
|
||||||
|
|
||||||
|
$node = new Node([
|
||||||
|
'maintenance_mode' => false,
|
||||||
|
]);
|
||||||
$server->setRelation('node', $node);
|
$server->setRelation('node', $node);
|
||||||
|
|
||||||
$this->setRequestAttribute('server', $server);
|
$this->setRequestAttribute('server', $server);
|
||||||
|
|
||||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||||
@ -43,10 +47,13 @@ class MaintenanceMiddlewareTest extends MiddlewareTestCase
|
|||||||
*/
|
*/
|
||||||
public function testHandleInMaintenanceMode(): void
|
public function testHandleInMaintenanceMode(): void
|
||||||
{
|
{
|
||||||
$server = Server::factory()->make();
|
$server = new Server();
|
||||||
$node = Node::factory()->make(['maintenance_mode' => 1]);
|
|
||||||
|
|
||||||
|
$node = new Node([
|
||||||
|
'maintenance_mode' => true,
|
||||||
|
]);
|
||||||
$server->setRelation('node', $node);
|
$server->setRelation('node', $node);
|
||||||
|
|
||||||
$this->setRequestAttribute('server', $server);
|
$this->setRequestAttribute('server', $server);
|
||||||
|
|
||||||
$this->response->shouldReceive('view')
|
$this->response->shouldReceive('view')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user