diff --git a/app/Actions/ActivityType.php b/app/Actions/ActivityType.php index 60b1630e0..8b5213a8b 100644 --- a/app/Actions/ActivityType.php +++ b/app/Actions/ActivityType.php @@ -53,4 +53,16 @@ class ActivityType const MFA_SETUP_METHOD = 'mfa_setup_method'; const MFA_REMOVE_METHOD = 'mfa_remove_method'; + + const WEBHOOK_CREATE = 'webhook_create'; + const WEBHOOK_UPDATE = 'webhook_update'; + const WEBHOOK_DELETE = 'webhook_delete'; + + /** + * Get all the possible values. + */ + public static function all(): array + { + return (new \ReflectionClass(static::class))->getConstants(); + } } diff --git a/app/Actions/Webhook.php b/app/Actions/Webhook.php index 6939b54d6..2d11584e6 100644 --- a/app/Actions/Webhook.php +++ b/app/Actions/Webhook.php @@ -2,10 +2,24 @@ namespace BookStack\Actions; +use BookStack\Interfaces\Loggable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -class Webhook extends Model +/** + * @property int $id + * @property string $name + * @property string $endpoint + */ +class Webhook extends Model implements Loggable { use HasFactory; + + /** + * Get the string descriptor for this item. + */ + public function logDescriptor(): string + { + return "({$this->id}) {$this->name}"; + } } diff --git a/app/Http/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php index 8745bf91d..15a31f312 100644 --- a/app/Http/Controllers/WebhookController.php +++ b/app/Http/Controllers/WebhookController.php @@ -2,15 +2,93 @@ namespace BookStack\Http\Controllers; +use BookStack\Actions\ActivityType; +use BookStack\Actions\Webhook; use Illuminate\Http\Request; class WebhookController extends Controller { + public function __construct() + { + $this->middleware([ + 'can:settings-manage', + ]); + } + /** * Show all webhooks configured in the system. */ public function index() { + // TODO - Get and pass webhooks return view('settings.webhooks.index'); } + + /** + * Show the view for creating a new webhook in the system. + */ + public function create() + { + return view('settings.webhooks.create'); + } + + /** + * Store a new webhook in the system. + */ + public function store(Request $request) + { + // TODO - Create webhook + $this->logActivity(ActivityType::WEBHOOK_CREATE, $webhook); + return redirect('/settings/webhooks'); + } + + /** + * Show the view to edit an existing webhook. + */ + public function edit(string $id) + { + /** @var Webhook $webhook */ + $webhook = Webhook::query()->findOrFail($id); + + return view('settings.webhooks.edit', ['webhook' => $webhook]); + } + + /** + * Update an existing webhook with the provided request data. + */ + public function update(Request $request, string $id) + { + /** @var Webhook $webhook */ + $webhook = Webhook::query()->findOrFail($id); + + // TODO - Update + + $this->logActivity(ActivityType::WEBHOOK_UPDATE, $webhook); + return redirect('/settings/webhooks'); + } + + /** + * Show the view to delete a webhook. + */ + public function delete(string $id) + { + /** @var Webhook $webhook */ + $webhook = Webhook::query()->findOrFail($id); + return view('settings.webhooks.delete', ['webhook' => $webhook]); + } + + /** + * Destroy a webhook from the system. + */ + public function destroy(string $id) + { + /** @var Webhook $webhook */ + $webhook = Webhook::query()->findOrFail($id); + + // TODO - Delete event type relations + $webhook->delete(); + + $this->logActivity(ActivityType::WEBHOOK_DELETE, $webhook); + return redirect('/settings/webhooks'); + } } diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 010ee04ba..fe348aba7 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -50,6 +50,7 @@ import templateManager from "./template-manager.js" import toggleSwitch from "./toggle-switch.js" import triLayout from "./tri-layout.js" import userSelect from "./user-select.js" +import webhookEvents from "./webhook-events"; import wysiwygEditor from "./wysiwyg-editor.js" const componentMapping = { @@ -105,6 +106,7 @@ const componentMapping = { "toggle-switch": toggleSwitch, "tri-layout": triLayout, "user-select": userSelect, + "webhook-events": webhookEvents, "wysiwyg-editor": wysiwygEditor, }; diff --git a/resources/js/components/webhook-events.js b/resources/js/components/webhook-events.js new file mode 100644 index 000000000..54080d36e --- /dev/null +++ b/resources/js/components/webhook-events.js @@ -0,0 +1,32 @@ + +/** + * Webhook Events + * Manages dynamic selection control in the webhook form interface. + * @extends {Component} + */ +class WebhookEvents { + + setup() { + this.checkboxes = this.$el.querySelectorAll('input[type="checkbox"]'); + this.allCheckbox = this.$refs.all; + + this.$el.addEventListener('change', event => { + if (event.target.checked && event.target === this.allCheckbox) { + this.deselectIndividualEvents(); + } else if (event.target.checked) { + this.allCheckbox.checked = false; + } + }); + } + + deselectIndividualEvents() { + for (const checkbox of this.checkboxes) { + if (checkbox !== this.allCheckbox) { + checkbox.checked = false; + } + } + } + +} + +export default WebhookEvents; \ No newline at end of file diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php index 50bda60bd..1919df706 100644 --- a/resources/lang/en/activities.php +++ b/resources/lang/en/activities.php @@ -51,6 +51,14 @@ return [ 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Webhooks + 'webhook_create' => 'created webhook', + 'webhook_create_notification' => 'Webhook successfully created', + 'webhook_update' => 'updated webhook', + 'webhook_update_notification' => 'Webhook successfully updated', + 'webhook_delete' => 'deleted webhook', + 'webhook_delete_notification' => 'Webhook successfully deleted', + // Other 'commented_on' => 'commented on', 'permissions_update' => 'updated permissions', diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 57cbe500e..6812075b3 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -236,6 +236,19 @@ return [ // Webhooks 'webhooks' => 'Webhooks', 'webhooks_create' => 'Create New Webhook', + 'webhooks_edit' => 'Edit Webhook', + 'webhooks_save' => 'Save Webhook', + 'webhooks_details' => 'Webhook Details', + 'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.', + 'webhooks_events' => 'Webhook Events', + 'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.', + 'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.', + 'webhooks_events_all' => 'All system events', + 'webhooks_name' => 'Webhook Name', + 'webhooks_endpoint' => 'Webhook Endpoint', + 'webhooks_delete' => 'Delete Webhook', + 'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.', + 'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?', //! If editing translations files directly please ignore this in all //! languages apart from en. Content will be auto-copied from en. diff --git a/resources/views/settings/webhooks/create.blade.php b/resources/views/settings/webhooks/create.blade.php new file mode 100644 index 000000000..b49afe415 --- /dev/null +++ b/resources/views/settings/webhooks/create.blade.php @@ -0,0 +1,16 @@ +@extends('layouts.simple') + +@section('body') + +
+ +
+ @include('settings.parts.navbar', ['selected' => 'webhooks']) +
+ +
+ @include('settings.webhooks.parts.form', ['title' => trans('settings.webhooks_create')]) +
+
+ +@stop diff --git a/resources/views/settings/webhooks/delete.blade.php b/resources/views/settings/webhooks/delete.blade.php new file mode 100644 index 000000000..a89b01171 --- /dev/null +++ b/resources/views/settings/webhooks/delete.blade.php @@ -0,0 +1,39 @@ +@extends('layouts.simple') + +@section('body') +
+ +
+ @include('settings.parts.navbar', ['selected' => 'webhooks']) +
+ +
+

{{ trans('settings.webhooks_delete') }}

+ +

{{ trans('settings.webhooks_delete_warning', ['webhookName' => $webhook->name]) }}

+ + +
id}") }}" method="POST"> + {!! csrf_field() !!} + {!! method_field('DELETE') !!} + +
+
+

+ {{ trans('settings.webhooks_delete_confirm') }} +

+
+
+ +
+
+ + +
+
+ +
+@stop diff --git a/resources/views/settings/webhooks/edit.blade.php b/resources/views/settings/webhooks/edit.blade.php new file mode 100644 index 000000000..d4e60cc14 --- /dev/null +++ b/resources/views/settings/webhooks/edit.blade.php @@ -0,0 +1,16 @@ +@extends('layouts.simple') + +@section('body') + +
+
+ @include('settings.parts.navbar', ['selected' => 'webhooks']) +
+ +
id}") }}" method="POST"> + {!! method_field('PUT') !!} + @include('settings.webhooks.parts.form', ['model' => $webhook, 'title' => trans('settings.webhooks_edit')]) +
+
+ +@stop diff --git a/resources/views/settings/webhooks/index.blade.php b/resources/views/settings/webhooks/index.blade.php index ca93cfeb0..8adf60835 100644 --- a/resources/views/settings/webhooks/index.blade.php +++ b/resources/views/settings/webhooks/index.blade.php @@ -14,7 +14,7 @@

{{ trans('settings.webhooks') }}

- {{ trans('settings.webhooks_create') }} + {{ trans('settings.webhooks_create') }}
diff --git a/resources/views/settings/webhooks/parts/form.blade.php b/resources/views/settings/webhooks/parts/form.blade.php new file mode 100644 index 000000000..935b01992 --- /dev/null +++ b/resources/views/settings/webhooks/parts/form.blade.php @@ -0,0 +1,57 @@ +{!! csrf_field() !!} + +
+

{{ $title }}

+ +
+ +
+
+ +

{{ trans('settings.webhooks_details_desc') }}

+
+
+
+ + @include('form.text', ['name' => 'name']) +
+
+ + @include('form.text', ['name' => 'endpoint']) +
+
+
+ +
+ +

{{ trans('settings.webhooks_events_desc') }}

+

{{ trans('settings.webhooks_events_warning') }}

+ +
+ +
+ +
+ +
+ @foreach(\BookStack\Actions\ActivityType::all() as $activityType) + + @endforeach +
+
+ +
+ +
+ {{ trans('common.cancel') }} + @if ($webhook->id ?? false) + id}") }}" class="button outline">{{ trans('settings.webhooks_delete') }} + @endif + +
+ +
diff --git a/routes/web.php b/routes/web.php index 627ce6523..d7e734c33 100644 --- a/routes/web.php +++ b/routes/web.php @@ -258,6 +258,12 @@ Route::middleware('auth')->group(function () { // Webhooks Route::get('/settings/webhooks', [WebhookController::class, 'index']); + Route::get('/settings/webhooks/create', [WebhookController::class, 'create']); + Route::post('/settings/webhooks/create', [WebhookController::class, 'store']); + Route::get('/settings/webhooks/{id}', [WebhookController::class, 'edit']); + Route::put('/settings/webhooks/{id}', [WebhookController::class, 'update']); + Route::get('/settings/webhooks/{id}/delete', [WebhookController::class, 'delete']); + Route::delete('/settings/webhooks/{id}', [WebhookController::class, 'destroy']); }); // MFA routes