fix(webhooks): add missing columns; add pre-save URL test endpoint
* The model+validation referenced tracked_clients and templates columns but they were never in the unifi_webhook_configs migration. Any save attempt that included those keys 500'd with "Unknown column". Added an additive migration (idempotent) that adds both as nullable json columns. * New POST /settings/webhooks/test-url endpoint takes a url+secret in the body and fires the standard test payload. Lets operators validate their endpoint before saving the row — useful when first wiring up Google Chat, Slack, etc. v1.5.4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "dashboard/unifi",
|
"name": "dashboard/unifi",
|
||||||
"description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform",
|
"description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform",
|
||||||
"version": "1.5.3",
|
"version": "1.5.4",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('unifi_webhook_configs', function (Blueprint $table) {
|
||||||
|
if (! Schema::hasColumn('unifi_webhook_configs', 'tracked_clients')) {
|
||||||
|
$table->json('tracked_clients')->nullable()->after('device_filter');
|
||||||
|
}
|
||||||
|
if (! Schema::hasColumn('unifi_webhook_configs', 'templates')) {
|
||||||
|
$table->json('templates')->nullable()->after('tracked_clients');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('unifi_webhook_configs', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('unifi_webhook_configs', 'templates')) {
|
||||||
|
$table->dropColumn('templates');
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('unifi_webhook_configs', 'tracked_clients')) {
|
||||||
|
$table->dropColumn('tracked_clients');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -67,6 +67,25 @@ class WebhookController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function test(WebhookConfig $webhook)
|
public function test(WebhookConfig $webhook)
|
||||||
|
{
|
||||||
|
return $this->fireTest($webhook->url, $webhook->secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test an arbitrary URL+secret before the webhook is saved. Lets the
|
||||||
|
* operator validate their endpoint from the form without first
|
||||||
|
* committing a row.
|
||||||
|
*/
|
||||||
|
public function testUrl(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'url' => 'required|url|max:500',
|
||||||
|
'secret' => 'nullable|string|max:255',
|
||||||
|
]);
|
||||||
|
return $this->fireTest($data['url'], $data['secret'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fireTest(string $url, ?string $secret)
|
||||||
{
|
{
|
||||||
$payload = [
|
$payload = [
|
||||||
'event' => 'test',
|
'event' => 'test',
|
||||||
@@ -75,13 +94,17 @@ class WebhookController extends Controller
|
|||||||
];
|
];
|
||||||
|
|
||||||
$headers = ['Content-Type' => 'application/json'];
|
$headers = ['Content-Type' => 'application/json'];
|
||||||
if ($webhook->secret) {
|
if ($secret) {
|
||||||
$headers['X-Webhook-Signature'] = hash_hmac('sha256', json_encode($payload), $webhook->secret);
|
$headers['X-Webhook-Signature'] = hash_hmac('sha256', json_encode($payload), $secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = \Illuminate\Support\Facades\Http::withHeaders($headers)->timeout(10)->post($webhook->url, $payload);
|
$response = \Illuminate\Support\Facades\Http::withHeaders($headers)->timeout(10)->post($url, $payload);
|
||||||
return response()->json(['ok' => true, 'status' => $response->status()]);
|
return response()->json([
|
||||||
|
'ok' => $response->successful(),
|
||||||
|
'status' => $response->status(),
|
||||||
|
'body' => mb_substr((string) $response->body(), 0, 500),
|
||||||
|
]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return response()->json(['ok' => false, 'error' => $e->getMessage()], 422);
|
return response()->json(['ok' => false, 'error' => $e->getMessage()], 422);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ Route::middleware(['web', 'auth', 'app.access:unifi'])
|
|||||||
Route::put('/settings/webhooks/{webhook}', [WebhookController::class, 'update']) ->name('webhooks.update');
|
Route::put('/settings/webhooks/{webhook}', [WebhookController::class, 'update']) ->name('webhooks.update');
|
||||||
Route::delete('/settings/webhooks/{webhook}', [WebhookController::class, 'destroy'])->name('webhooks.destroy');
|
Route::delete('/settings/webhooks/{webhook}', [WebhookController::class, 'destroy'])->name('webhooks.destroy');
|
||||||
Route::post('/settings/webhooks/{webhook}/test', [WebhookController::class, 'test']) ->name('webhooks.test');
|
Route::post('/settings/webhooks/{webhook}/test', [WebhookController::class, 'test']) ->name('webhooks.test');
|
||||||
|
Route::post('/settings/webhooks/test-url', [WebhookController::class, 'testUrl'])->name('webhooks.test-url');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user