Initial controller/views for webhooks management

This commit is contained in:
Dan Brown 2021-12-08 14:29:42 +00:00
parent a3a3055695
commit 4621d8bcc5
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
13 changed files with 295 additions and 2 deletions

View File

@ -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();
}
}

View File

@ -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}";
}
}

View File

@ -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');
}
}

View File

@ -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,
};

View File

@ -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;

View File

@ -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',

View File

@ -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.

View File

@ -0,0 +1,16 @@
@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

View File

@ -0,0 +1,39 @@
@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

View File

@ -0,0 +1,16 @@
@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

View File

@ -14,7 +14,7 @@
<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>

View File

@ -0,0 +1,57 @@
{!! 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>

View File

@ -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