* The Test URL button was POSTing a generic {event, timestamp, data}
envelope to every endpoint. Google Chat / Slack / Discord / Teams
reject anything that isn't their specific shape — so a successful
Laravel request still got a 400 back from the platform, making the
test look broken. The real webhook events already handle this via
WebhookCheckService::formatPayloadForPlatform; that helper is now
exposed as a public static (buildPlatformPayload) and the test
endpoint uses the same code path, so the test exercises the same
format real events will.
* unifi_device_states was missing a consecutive_count column the
WebhookCheckService inserts on every snapshot capture. The scheduler
was throwing "Unknown column 'consecutive_count'" once a minute.
Added an idempotent migration.
v1.6.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
119 lines
4.5 KiB
PHP
119 lines
4.5 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)
|
|
{
|
|
$message = '✅ Test webhook from ' . config('app.name') . ' — endpoint is reachable.';
|
|
$genericPayload = [
|
|
'event' => 'test',
|
|
'timestamp' => now()->toIso8601String(),
|
|
'message' => $message,
|
|
'data' => ['message' => $message],
|
|
];
|
|
// Shape the payload to match the target platform (Google Chat,
|
|
// Slack, Discord, Teams) so the test exercises the same code
|
|
// path real events use.
|
|
$payload = \Dashboard\Unifi\Services\WebhookCheckService::buildPlatformPayload($url, $message, $genericPayload);
|
|
|
|
$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);
|
|
}
|
|
}
|
|
}
|