Files
central-unifi/src/Http/Controllers/WebhookController.php
jwed c89adeea97 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>
2026-05-24 17:59:44 -04:00

113 lines
4.2 KiB
PHP

<?php
namespace Dashboard\Unifi\Http\Controllers;
use Dashboard\Unifi\Models\WebhookConfig;
use Dashboard\Unifi\Services\WebhookCheckService;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Inertia\Inertia;
class WebhookController extends Controller
{
public function index()
{
return Inertia::render('Unifi/Webhooks', [
'webhooks' => WebhookConfig::latest()->get(),
'recentLogs' => \Dashboard\Unifi\Models\WebhookLog::with('config:id,name')
->latest('fired_at')->take(50)->get(),
'eventTypes' => WebhookCheckService::EVENTS,
'templateVars' => WebhookCheckService::TEMPLATE_VARS,
'defaultTemplates' => WebhookCheckService::DEFAULT_TEMPLATES,
]);
}
public function store(Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:255',
'url' => 'required|url|max:500',
'secret' => 'nullable|string|max:255',
'is_active' => 'boolean',
'events' => 'required|array|min:1',
'events.*' => 'string|in:' . implode(',', array_keys(WebhookCheckService::EVENTS)),
'thresholds' => 'nullable|array',
'device_filter' => 'nullable|array',
'tracked_clients' => 'nullable|array',
'templates' => 'nullable|array',
'cooldown_minutes' => 'integer|min:1|max:1440',
]);
WebhookConfig::create($data);
return back()->with('success', 'Webhook created.');
}
public function update(Request $request, WebhookConfig $webhook)
{
$data = $request->validate([
'name' => 'required|string|max:255',
'url' => 'required|url|max:500',
'secret' => 'nullable|string|max:255',
'is_active' => 'boolean',
'events' => 'required|array|min:1',
'events.*' => 'string|in:' . implode(',', array_keys(WebhookCheckService::EVENTS)),
'thresholds' => 'nullable|array',
'device_filter' => 'nullable|array',
'tracked_clients' => 'nullable|array',
'templates' => 'nullable|array',
'cooldown_minutes' => 'integer|min:1|max:1440',
]);
$webhook->update($data);
return back()->with('success', 'Webhook updated.');
}
public function destroy(WebhookConfig $webhook)
{
$webhook->delete();
return back()->with('success', 'Webhook deleted.');
}
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 = [
'event' => 'test',
'timestamp' => now()->toIso8601String(),
'data' => ['message' => 'This is a test webhook from ' . config('app.name')],
];
$headers = ['Content-Type' => 'application/json'];
if ($secret) {
$headers['X-Webhook-Signature'] = hash_hmac('sha256', json_encode($payload), $secret);
}
try {
$response = \Illuminate\Support\Facades\Http::withHeaders($headers)->timeout(10)->post($url, $payload);
return response()->json([
'ok' => $response->successful(),
'status' => $response->status(),
'body' => mb_substr((string) $response->body(), 0, 500),
]);
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'error' => $e->getMessage()], 422);
}
}
}