From 826a304071aa0370ef0ec527fd7b74bc05f58de4 Mon Sep 17 00:00:00 2001 From: Brian Manifold Date: Wed, 3 Sep 2025 20:56:01 -0700 Subject: [PATCH] feat(portal): enable outdated gateway email (#10281) Enables 'outdated gateway' notifications for all accounts. Closes #8361 --- elixir/apps/domain/lib/domain/accounts.ex | 8 ++++ .../lib/domain/accounts/account/changeset.ex | 20 ++++++++ .../apps/domain/lib/domain/accounts/config.ex | 33 +++++++++++++ .../accounts/config/notifications/email.ex | 2 +- elixir/apps/domain/lib/domain/mailer.ex | 2 +- .../notifications/jobs/outdated_gateways.ex | 2 +- ...able_outdated_gateway_email_by_default.exs | 47 +++++++++++++++++++ .../apps/web/lib/web/live/settings/account.ex | 16 +------ 8 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 elixir/apps/domain/priv/repo/migrations/20250902190940_enable_outdated_gateway_email_by_default.exs diff --git a/elixir/apps/domain/lib/domain/accounts.ex b/elixir/apps/domain/lib/domain/accounts.ex index 32b4bfd59..b66b4046a 100644 --- a/elixir/apps/domain/lib/domain/accounts.ex +++ b/elixir/apps/domain/lib/domain/accounts.ex @@ -32,6 +32,14 @@ defmodule Domain.Accounts do |> Repo.all() end + # TODO: This will need to be updated once more notifications are available + def all_accounts_pending_notification! do + Account.Query.not_disabled() + |> Account.Query.by_notification_enabled("outdated_gateway") + |> Account.Query.by_notification_last_notified("outdated_gateway", 24) + |> Repo.all() + end + def fetch_account_by_id(id, %Auth.Subject{} = subject, opts \\ []) do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_own_account_permission()), true <- Repo.valid_uuid?(id) do diff --git a/elixir/apps/domain/lib/domain/accounts/account/changeset.ex b/elixir/apps/domain/lib/domain/accounts/account/changeset.ex index 68a8a1bfa..ea8c6ed3f 100644 --- a/elixir/apps/domain/lib/domain/accounts/account/changeset.ex +++ b/elixir/apps/domain/lib/domain/accounts/account/changeset.ex @@ -18,6 +18,7 @@ defmodule Domain.Accounts.Account.Changeset do %Account{} |> cast(attrs, [:name, :legal_name, :slug]) |> changeset() + |> put_default_config() end def update_profile_and_config(%Account{} = account, attrs) do @@ -121,4 +122,23 @@ defmodule Domain.Accounts.Account.Changeset do {:error, "Account ID or Slug contains invalid characters"} end end + + defp put_default_config(changeset) do + case get_change(changeset, :config) do + nil -> + put_change(changeset, :config, Config.default_config()) + + %Ecto.Changeset{} = config_changeset -> + config = Ecto.Changeset.apply_changes(config_changeset) + updated_config = Config.ensure_defaults(config) + put_change(changeset, :config, updated_config) + + %Config{} = config -> + updated_config = Config.ensure_defaults(config) + put_change(changeset, :config, updated_config) + + _ -> + changeset + end + end end diff --git a/elixir/apps/domain/lib/domain/accounts/config.ex b/elixir/apps/domain/lib/domain/accounts/config.ex index 3823cd9d7..edd45c424 100644 --- a/elixir/apps/domain/lib/domain/accounts/config.ex +++ b/elixir/apps/domain/lib/domain/accounts/config.ex @@ -23,4 +23,37 @@ defmodule Domain.Accounts.Config do end def supported_dns_protocols, do: ~w[ip_port]a + + @doc """ + Returns a default config with defaults set + """ + def default_config do + %__MODULE__{ + notifications: %__MODULE__.Notifications{ + outdated_gateway: %Domain.Accounts.Config.Notifications.Email{enabled: true} + } + } + end + + @doc """ + Ensures a config has proper defaults + """ + def ensure_defaults(%__MODULE__{} = config) do + notifications = config.notifications || %__MODULE__.Notifications{} + + outdated_gateway = + notifications.outdated_gateway || %Domain.Accounts.Config.Notifications.Email{enabled: true} + + outdated_gateway = + case outdated_gateway.enabled do + nil -> %{outdated_gateway | enabled: true} + _ -> outdated_gateway + end + + notifications = %{notifications | outdated_gateway: outdated_gateway} + + %{config | notifications: notifications} + end + + def ensure_defaults(nil), do: default_config() end diff --git a/elixir/apps/domain/lib/domain/accounts/config/notifications/email.ex b/elixir/apps/domain/lib/domain/accounts/config/notifications/email.ex index 09c8537d3..a300d9a74 100644 --- a/elixir/apps/domain/lib/domain/accounts/config/notifications/email.ex +++ b/elixir/apps/domain/lib/domain/accounts/config/notifications/email.ex @@ -4,7 +4,7 @@ defmodule Domain.Accounts.Config.Notifications.Email do @primary_key false embedded_schema do - field :enabled, :boolean + field :enabled, :boolean, default: true field :last_notified, :utc_datetime end end diff --git a/elixir/apps/domain/lib/domain/mailer.ex b/elixir/apps/domain/lib/domain/mailer.ex index 44b1c7dea..807c5a1ef 100644 --- a/elixir/apps/domain/lib/domain/mailer.ex +++ b/elixir/apps/domain/lib/domain/mailer.ex @@ -91,6 +91,6 @@ defmodule Domain.Mailer do |> Keyword.fetch!(:from_email) Email.new() - |> Email.from(from_email) + |> Email.from({"Firezone Notifications", from_email}) end end diff --git a/elixir/apps/domain/lib/domain/notifications/jobs/outdated_gateways.ex b/elixir/apps/domain/lib/domain/notifications/jobs/outdated_gateways.ex index ecfc15a2b..b92dbfcbd 100644 --- a/elixir/apps/domain/lib/domain/notifications/jobs/outdated_gateways.ex +++ b/elixir/apps/domain/lib/domain/notifications/jobs/outdated_gateways.ex @@ -24,7 +24,7 @@ defmodule Domain.Notifications.Jobs.OutdatedGateways do end defp run_check do - Accounts.all_active_paid_accounts_pending_notification!() + Accounts.all_accounts_pending_notification!() |> Enum.each(fn account -> all_online_gateways_for_account(account) |> Enum.filter(&Gateways.gateway_outdated?/1) diff --git a/elixir/apps/domain/priv/repo/migrations/20250902190940_enable_outdated_gateway_email_by_default.exs b/elixir/apps/domain/priv/repo/migrations/20250902190940_enable_outdated_gateway_email_by_default.exs new file mode 100644 index 000000000..f8d435cc8 --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20250902190940_enable_outdated_gateway_email_by_default.exs @@ -0,0 +1,47 @@ +defmodule Domain.Repo.Migrations.EnableOutdatedGatewayEmailByDefault do + use Ecto.Migration + + def up do + execute(""" + UPDATE accounts + SET config = jsonb_set( + jsonb_set( + COALESCE(config, '{}'::jsonb), + '{notifications}', + CASE + WHEN config->'notifications' IS NULL OR config->'notifications' = 'null'::jsonb THEN '{}'::jsonb + ELSE config->'notifications' + END, + true + ), + '{notifications,outdated_gateway}', + jsonb_build_object('enabled', true), + true + ) + WHERE deleted_at IS NULL + AND disabled_at IS NULL + """) + end + + def down do + execute(""" + UPDATE accounts + SET config = jsonb_set( + jsonb_set( + COALESCE(config, '{}'::jsonb), + '{notifications}', + CASE + WHEN config->'notifications' IS NULL OR config->'notifications' = 'null'::jsonb THEN '{}'::jsonb + ELSE config->'notifications' + END, + true + ), + '{notifications,outdated_gateway}', + jsonb_build_object('enabled', false), + true + ) + WHERE deleted_at IS NULL + AND disabled_at IS NULL + """) + end +end diff --git a/elixir/apps/web/lib/web/live/settings/account.ex b/elixir/apps/web/lib/web/live/settings/account.ex index 4e343a64e..9e2f95cda 100644 --- a/elixir/apps/web/lib/web/live/settings/account.ex +++ b/elixir/apps/web/lib/web/live/settings/account.ex @@ -53,13 +53,9 @@ defmodule Web.Settings.Account do Notifications <:action> - <.edit_button - :if={@account_type != "Starter"} - navigate={~p"/#{@account}/settings/account/notifications/edit"} - > + <.edit_button navigate={~p"/#{@account}/settings/account/notifications/edit"}> Edit Notifications - <.upgrade_badge :if={@account_type == "Starter"} account={@account} /> <:content>
@@ -155,16 +151,6 @@ defmodule Web.Settings.Account do """ end - defp upgrade_badge(assigns) do - ~H""" - <.link navigate={~p"/#{@account}/settings/billing"} class="text-sm text-primary-500"> - <.badge type="primary" title="Feature available on a higher pricing plan"> - <.icon name="hero-lock-closed" class="w-3.5 h-3.5 mr-1" /> UPGRADE TO UNLOCK - - - """ - end - defp notification_badge(assigns) do ~H""" <.badge type={if @notification.enabled, do: "success", else: "neutral"}>