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();
+ }
}
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}";
+ }
}
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');
+ }
}
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 = {
"toggle-switch": toggleSwitch,
"tri-layout": triLayout,
"user-select": userSelect,
+ "webhook-events": webhookEvents,
"wysiwyg-editor": wysiwygEditor,
};
--- /dev/null
+
+/**
+ * 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
'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',
// 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.
--- /dev/null
+@extends('layouts.simple')
+
+@section('body')
+
+ <div class="container small">
+
+ <div class="py-m">
+ @include('settings.parts.navbar', ['selected' => 'webhooks'])
+ </div>
+
+ <form action="{{ url("/settings/webhooks/new") }}" method="POST">
+ @include('settings.webhooks.parts.form', ['title' => trans('settings.webhooks_create')])
+ </form>
+ </div>
+
+@stop
--- /dev/null
+@extends('layouts.simple')
+
+@section('body')
+ <div class="container small">
+
+ <div class="py-m">
+ @include('settings.parts.navbar', ['selected' => 'webhooks'])
+ </div>
+
+ <div class="card content-wrap auto-height">
+ <h1 class="list-heading"> {{ trans('settings.webhooks_delete') }}</h1>
+
+ <p>{{ trans('settings.webhooks_delete_warning', ['webhookName' => $webhook->name]) }}</p>
+
+
+ <form action="{{ url("/settings/webhooks/{$role->id}") }}" method="POST">
+ {!! csrf_field() !!}
+ {!! method_field('DELETE') !!}
+
+ <div class="grid half v-center">
+ <div>
+ <p class="text-neg">
+ <strong>{{ trans('settings.webhooks_delete_confirm') }}</strong>
+ </p>
+ </div>
+ <div>
+ <div class="form-group text-right">
+ <a href="{{ url("/settings/webhooks/{$role->id}") }}" class="button outline">{{ trans('common.cancel') }}</a>
+ <button type="submit" class="button">{{ trans('common.confirm') }}</button>
+ </div>
+ </div>
+ </div>
+
+
+ </form>
+ </div>
+
+ </div>
+@stop
--- /dev/null
+@extends('layouts.simple')
+
+@section('body')
+
+ <div class="container small">
+ <div class="py-m">
+ @include('settings.parts.navbar', ['selected' => 'webhooks'])
+ </div>
+
+ <form action="{{ url("/settings/webhooks/{$webhook->id}") }}" method="POST">
+ {!! method_field('PUT') !!}
+ @include('settings.webhooks.parts.form', ['model' => $webhook, 'title' => trans('settings.webhooks_edit')])
+ </form>
+ </div>
+
+@stop
<h1 class="list-heading">{{ trans('settings.webhooks') }}</h1>
<div class="text-right">
- <a href="{{ url("/settings/webhooks/new") }}" class="button outline">{{ trans('settings.webhooks_create') }}</a>
+ <a href="{{ url("/settings/webhooks/create") }}" class="button outline">{{ trans('settings.webhooks_create') }}</a>
</div>
</div>
--- /dev/null
+{!! csrf_field() !!}
+
+<div class="card content-wrap auto-height">
+ <h1 class="list-heading">{{ $title }}</h1>
+
+ <div class="setting-list">
+
+ <div class="grid half">
+ <div>
+ <label class="setting-list-label">{{ trans('settings.webhooks_details') }}</label>
+ <p class="small">{{ trans('settings.webhooks_details_desc') }}</p>
+ </div>
+ <div>
+ <div class="form-group">
+ <label for="name">{{ trans('settings.webhooks_name') }}</label>
+ @include('form.text', ['name' => 'name'])
+ </div>
+ <div class="form-group">
+ <label for="endpoint">{{ trans('settings.webhooks_endpoint') }}</label>
+ @include('form.text', ['name' => 'endpoint'])
+ </div>
+ </div>
+ </div>
+
+ <div component="webhook-events">
+ <label class="setting-list-label">{{ trans('settings.webhooks_events') }}</label>
+ <p class="small">{{ trans('settings.webhooks_events_desc') }}</p>
+ <p class="text-warn small">{{ trans('settings.webhooks_events_warning') }}</p>
+
+ <div>
+ <label><input type="checkbox"
+ name="events[]"
+ value="all"
+ refs="webhook-events@all">
+ {{ trans('settings.webhooks_events_all') }}</label>
+ </div>
+
+ <hr class="my-m">
+
+ <div class="dual-column-content">
+ @foreach(\BookStack\Actions\ActivityType::all() as $activityType)
+ <label><input type="checkbox" name="events[]" value="{{ $activityType }}">{{ $activityType }}</label>
+ @endforeach
+ </div>
+ </div>
+
+ </div>
+
+ <div class="form-group text-right">
+ <a href="{{ url("/settings/webhooks") }}" class="button outline">{{ trans('common.cancel') }}</a>
+ @if ($webhook->id ?? false)
+ <a href="{{ url("/settings/roles/delete/{$webhook->id}") }}" class="button outline">{{ trans('settings.webhooks_delete') }}</a>
+ @endif
+ <button type="submit" class="button">{{ trans('settings.webhooks_save') }}</button>
+ </div>
+
+</div>
// 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