From 933d51e3d0977a6ddca5524c92be82f9bf0a1beb Mon Sep 17 00:00:00 2001 From: Jamil Date: Tue, 24 Jun 2025 11:35:06 -0700 Subject: [PATCH] feat(portal): send account_slug in gateway init (#9653) Adds the `account_slug` to the gateway's `init` message. When the account slug is changed, the gateway's socket is disconnected using the same mechanism as gateway deletion, which causes the gateway to reconnect immediately and receive a new `init`. Related: #9545 --- elixir/apps/api/lib/api/gateway/channel.ex | 3 +++ .../apps/api/test/api/gateway/channel_test.exs | 8 +++++++- .../domain/lib/domain/events/hooks/accounts.ex | 11 +++++++++++ .../domain/lib/domain/gateways/presence.ex | 1 + elixir/apps/domain/lib/domain/pubsub.ex | 18 ++++++++++++++++++ .../test/domain/events/hooks/accounts_test.exs | 16 ++++++++++++++++ 6 files changed, 56 insertions(+), 1 deletion(-) diff --git a/elixir/apps/api/lib/api/gateway/channel.ex b/elixir/apps/api/lib/api/gateway/channel.ex index b4d76e8e7..21091bde2 100644 --- a/elixir/apps/api/lib/api/gateway/channel.ex +++ b/elixir/apps/api/lib/api/gateway/channel.ex @@ -48,7 +48,10 @@ defmodule API.Gateway.Channel do :ok = Enum.each(relays, &Domain.Relays.subscribe_to_relay_presence/1) :ok = maybe_subscribe_for_relays_presence(relays, socket) + account = Domain.Accounts.fetch_account_by_id!(socket.assigns.gateway.account_id) + push(socket, "init", %{ + account_slug: account.slug, interface: Views.Interface.render(socket.assigns.gateway), relays: Views.Relay.render_many(relays, relay_credentials_expire_at), config: %{ diff --git a/elixir/apps/api/test/api/gateway/channel_test.exs b/elixir/apps/api/test/api/gateway/channel_test.exs index cc67a7ac2..7f2209efc 100644 --- a/elixir/apps/api/test/api/gateway/channel_test.exs +++ b/elixir/apps/api/test/api/gateway/channel_test.exs @@ -58,17 +58,23 @@ defmodule API.Gateway.ChannelTest do assert is_number(online_at) end - test "sends list of resources after join", %{ + test "sends init message after join", %{ + account: account, gateway: gateway } do assert_push "init", %{ + account_slug: account_slug, interface: interface, + relays: relays, config: %{ ipv4_masquerade_enabled: true, ipv6_masquerade_enabled: true } } + assert account_slug == account.slug + assert relays == [] + assert interface == %{ ipv4: gateway.ipv4, ipv6: gateway.ipv6 diff --git a/elixir/apps/domain/lib/domain/events/hooks/accounts.ex b/elixir/apps/domain/lib/domain/events/hooks/accounts.ex index 676154feb..029b1f461 100644 --- a/elixir/apps/domain/lib/domain/events/hooks/accounts.ex +++ b/elixir/apps/domain/lib/domain/events/hooks/accounts.ex @@ -6,6 +6,17 @@ defmodule Domain.Events.Hooks.Accounts do @impl true def on_insert(_data), do: :ok + # Account slug changed - disconnect gateways for updated init + + @impl true + def on_update(%{"slug" => old_slug}, %{"slug" => slug, "id" => account_id} = _data) + when old_slug != slug do + # Technically we could push a :slug_changed message to the Gateways here, + # but at the time of writing, disconnecting and reconnecting is safer to ensure + # all relevant state on the Gateway is updated correctly. + PubSub.Account.Gateways.disconnect(account_id) + end + # Account disabled - disconnect clients @impl true def on_update( diff --git a/elixir/apps/domain/lib/domain/gateways/presence.ex b/elixir/apps/domain/lib/domain/gateways/presence.ex index 4082103f8..6725b5ee5 100644 --- a/elixir/apps/domain/lib/domain/gateways/presence.ex +++ b/elixir/apps/domain/lib/domain/gateways/presence.ex @@ -10,6 +10,7 @@ defmodule Domain.Gateways.Presence do with {:ok, _} <- __MODULE__.Group.track(gateway.group_id, gateway.id), {:ok, _} <- __MODULE__.Account.track(gateway.account_id, gateway.id) do :ok = PubSub.Gateway.subscribe(gateway.id) + :ok = PubSub.Account.Gateways.subscribe(gateway.account_id) end end diff --git a/elixir/apps/domain/lib/domain/pubsub.ex b/elixir/apps/domain/lib/domain/pubsub.ex index 2a8479c5e..23a9dd709 100644 --- a/elixir/apps/domain/lib/domain/pubsub.ex +++ b/elixir/apps/domain/lib/domain/pubsub.ex @@ -84,6 +84,24 @@ defmodule Domain.PubSub do end end + defmodule Gateways do + def subscribe(account_id) do + account_id + |> topic() + |> Domain.PubSub.subscribe() + end + + def disconnect(account_id) do + account_id + |> topic() + |> Domain.PubSub.broadcast("disconnect") + end + + defp topic(account_id) do + Atom.to_string(__MODULE__) <> ":" <> account_id + end + end + defmodule Policies do def subscribe(account_id) do account_id diff --git a/elixir/apps/domain/test/domain/events/hooks/accounts_test.exs b/elixir/apps/domain/test/domain/events/hooks/accounts_test.exs index 9dd348333..e172a8c37 100644 --- a/elixir/apps/domain/test/domain/events/hooks/accounts_test.exs +++ b/elixir/apps/domain/test/domain/events/hooks/accounts_test.exs @@ -13,10 +13,25 @@ defmodule Domain.Events.Hooks.AccountsTest do end describe "update/2" do + test "disconnects gateways if slug changes" do + account = Fixtures.Accounts.create_account() + gateway = Fixtures.Gateways.create_gateway(account: account) + :ok = Domain.Gateways.Presence.connect(gateway) + + old_data = %{"slug" => "old"} + data = %{"slug" => "new", "id" => account.id} + + assert :ok == on_update(old_data, data) + + assert_receive "disconnect" + end + test "sends :config_changed if config changes" do account = Fixtures.Accounts.create_account() + gateway = Fixtures.Gateways.create_gateway(account: account) :ok = Domain.PubSub.Account.subscribe(account.id) + :ok = Domain.Gateways.Presence.connect(gateway) old_data = %{ "id" => account.id, @@ -33,6 +48,7 @@ defmodule Domain.Events.Hooks.AccountsTest do assert :ok == on_update(old_data, data) assert_receive :config_changed + refute_receive "disconnect" end test "does not send :config_changed if config does not change" do