101 lines
3.1 KiB
PHP
101 lines
3.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Addon;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Install/uninstall integrations and reflect their backend connection status. Addons are opt-in:
|
|
* installing one only records intent + config here — the actual bridge (e.g. the ring-mqtt
|
|
* sidecar) runs as its own container and reports health over MQTT.
|
|
*/
|
|
class AddonService
|
|
{
|
|
/** Cache key the ingest gate reads to avoid a DB hit per message. */
|
|
public static function installedCacheKey(string $key): string
|
|
{
|
|
return "addon:{$key}:installed";
|
|
}
|
|
|
|
public function row(string $key): Addon
|
|
{
|
|
return Addon::firstOrCreate(['key' => $key]);
|
|
}
|
|
|
|
public function install(string $key): Addon
|
|
{
|
|
if (! AddonRegistry::has($key)) {
|
|
abort(404);
|
|
}
|
|
|
|
$addon = $this->row($key);
|
|
$addon->forceFill(['installed' => true])->save();
|
|
|
|
// Invalidate the ingest gate immediately — otherwise messages are dropped for up to the
|
|
// cache TTL right after install (and, on uninstall below, devices keep being created).
|
|
Cache::forget(self::installedCacheKey($key));
|
|
|
|
return $addon;
|
|
}
|
|
|
|
public function uninstall(string $key): void
|
|
{
|
|
$addon = Addon::where('key', $key)->first();
|
|
|
|
// Wipe stored secrets on uninstall — no orphaned tokens left encrypted at rest.
|
|
$addon?->forceFill([
|
|
'installed' => false,
|
|
'status' => 'disconnected',
|
|
'status_detail' => null,
|
|
'config' => null,
|
|
'connected_at' => null,
|
|
])->save();
|
|
|
|
Cache::forget(self::installedCacheKey($key));
|
|
}
|
|
|
|
/**
|
|
* Persist the credentials the backend bridge needs. We do NOT talk to Ring ourselves
|
|
* (no local API; the ring-mqtt sidecar owns the OAuth + 2FA dance) — we only hand the
|
|
* bridge what it asked for and mark the addon as connecting.
|
|
*
|
|
* @param array<string, mixed> $config
|
|
*/
|
|
public function saveConnection(string $key, array $config): Addon
|
|
{
|
|
$addon = $this->install($key);
|
|
|
|
$existing = $addon->config ?? [];
|
|
// Drop blank fields so re-submitting without the password doesn't wipe a stored one.
|
|
$merged = array_merge($existing, array_filter($config, fn ($v) => $v !== null && $v !== ''));
|
|
|
|
$addon->forceFill([
|
|
'config' => $merged,
|
|
'status' => 'connecting',
|
|
'status_detail' => null,
|
|
])->save();
|
|
|
|
return $addon;
|
|
}
|
|
|
|
/** Reflect a status line the bridge published over MQTT (online/offline/error). */
|
|
public function markStatus(string $key, string $status, ?string $detail = null): void
|
|
{
|
|
$addon = Addon::where('key', $key)->where('installed', true)->first();
|
|
|
|
if ($addon === null) {
|
|
return;
|
|
}
|
|
|
|
DB::transaction(function () use ($addon, $status, $detail) {
|
|
$addon->forceFill([
|
|
'status' => $status,
|
|
'status_detail' => $detail,
|
|
'connected_at' => $status === 'connected' ? now() : $addon->connected_at,
|
|
])->save();
|
|
});
|
|
}
|
|
}
|