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