+
<.input
type="select"
label="Protocol"
@@ -69,21 +74,42 @@ defmodule Web.Settings.DNS do
value={dns[:protocol].value}
/>
-
+
<.input
label="Address"
field={dns[:address]}
placeholder="DNS Server Address"
/>
+
+
+
+ <.icon name="hero-trash" class="text-red-500 w-6 h-6 relative top-2" />
+
+
+
- <% errors =
- translate_errors(
- @form.source.changes.config.errors,
- :clients_upstream_dns
- ) %>
- <.error :for={error <- errors} data-validation-error-for="clients_upstream_dns">
+
+ <.button
+ class="mt-6 w-full"
+ type="button"
+ style="info"
+ name={"#{config.name}[clients_upstream_dns_sort][]"}
+ value="new"
+ phx-click={JS.dispatch("change")}
+ >
+ New DNS Server
+
+ <.error
+ :for={error <- dns_config_errors(@form.source.changes)}
+ data-validation-error-for="clients_upstream_dns"
+ >
<%= error %>
@@ -106,103 +132,35 @@ defmodule Web.Settings.DNS do
end
def handle_event("change", %{"account" => attrs}, socket) do
- changeset =
+ form =
Accounts.change_account(socket.assigns.account, attrs)
- |> maybe_append_empty_embed()
- |> filter_errors()
|> Map.put(:action, :validate)
+ |> to_form()
- {:noreply, assign(socket, form: to_form(changeset))}
+ {:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"account" => attrs}, socket) do
- attrs = remove_empty_servers(attrs)
-
with {:ok, account} <-
Accounts.update_account(socket.assigns.account, attrs, socket.assigns.subject) do
form =
Accounts.change_account(account, %{})
- |> maybe_append_empty_embed()
|> to_form()
+ socket = put_flash(socket, :success, "Save successful!")
+
{:noreply, assign(socket, account: account, form: form)}
else
{:error, changeset} ->
- changeset =
+ form =
changeset
- |> maybe_append_empty_embed()
- |> filter_errors()
|> Map.put(:action, :validate)
+ |> to_form()
- {:noreply, assign(socket, form: to_form(changeset))}
+ {:noreply, assign(socket, form: form)}
end
end
- defp filter_errors(changeset) do
- update_clients_upstream_dns(changeset, fn
- clients_upstream_dns_changesets ->
- remove_errors(clients_upstream_dns_changesets, :address, "can't be blank")
- end)
- end
-
- defp remove_errors(changesets, field, message) do
- Enum.map(changesets, fn changeset ->
- errors =
- Enum.filter(changeset.errors, fn
- {^field, {^message, _}} -> false
- {_, _} -> true
- end)
-
- %{changeset | errors: errors}
- end)
- end
-
- defp maybe_append_empty_embed(changeset) do
- update_clients_upstream_dns(changeset, fn
- clients_upstream_dns_changesets ->
- last_client_upstream_dns_changeset = List.last(clients_upstream_dns_changesets)
-
- with true <- last_client_upstream_dns_changeset != nil,
- {_data_or_changes, last_address} <-
- Ecto.Changeset.fetch_field(last_client_upstream_dns_changeset, :address),
- true <- last_address in [nil, ""] do
- clients_upstream_dns_changesets
- else
- _other -> clients_upstream_dns_changesets ++ [%Accounts.Config.ClientsUpstreamDNS{}]
- end
- end)
- end
-
- defp update_clients_upstream_dns(changeset, cb) do
- config_changeset = Ecto.Changeset.get_embed(changeset, :config)
-
- clients_upstream_dns_changeset =
- Ecto.Changeset.get_embed(config_changeset, :clients_upstream_dns)
-
- config_changeset =
- Ecto.Changeset.put_embed(
- config_changeset,
- :clients_upstream_dns,
- cb.(clients_upstream_dns_changeset)
- )
-
- Ecto.Changeset.put_embed(changeset, :config, config_changeset)
- end
-
- defp remove_empty_servers(attrs) do
- update_in(attrs, [Access.key("config", %{}), "clients_upstream_dns"], fn
- nil ->
- nil
-
- servers ->
- Map.filter(servers, fn
- {_index, %{"address" => ""}} -> false
- {_index, %{"address" => nil}} -> false
- _ -> true
- end)
- end)
- end
-
defp dns_options do
supported_dns_protocols = Enum.map(Accounts.Config.supported_dns_protocols(), &to_string/1)
@@ -218,4 +176,12 @@ defmodule Web.Settings.DNS do
end
end)
end
+
+ defp dns_config_errors(changes) when changes == %{} do
+ []
+ end
+
+ defp dns_config_errors(changes) do
+ translate_errors(changes.config.errors, :clients_upstream_dns)
+ end
end
diff --git a/elixir/apps/web/lib/web/live/sites/gateways/index.ex b/elixir/apps/web/lib/web/live/sites/gateways/index.ex
index d01c22c6c..9cb66c609 100644
--- a/elixir/apps/web/lib/web/live/sites/gateways/index.ex
+++ b/elixir/apps/web/lib/web/live/sites/gateways/index.ex
@@ -84,11 +84,14 @@ defmodule Web.Sites.Gateways.Index do
<%= gateway.name %>
- <:col :let={gateway} label="remote iP">
+ <:col :let={gateway} label="remote ip">
<%= gateway.last_seen_remote_ip %>
+ <:col :let={gateway} label="version">
+ <%= gateway.last_seen_version %>
+
<:col :let={gateway} label="status">
<.connection_status schema={gateway} />
diff --git a/elixir/apps/web/lib/web/live/sites/show.ex b/elixir/apps/web/lib/web/live/sites/show.ex
index 1b965059b..15274db60 100644
--- a/elixir/apps/web/lib/web/live/sites/show.ex
+++ b/elixir/apps/web/lib/web/live/sites/show.ex
@@ -180,6 +180,10 @@ defmodule Web.Sites.Show do
<%= gateway.last_seen_remote_ip %>
+ <:col :let={gateway} label="version">
+ <.version_status outdated={Gateways.gateway_outdated?(gateway)} />
+ <%= gateway.last_seen_version %>
+
<:col :let={gateway} label="status">
<.connection_status schema={gateway} />
@@ -324,4 +328,23 @@ defmodule Web.Sites.Show do
{:ok, _group} = Gateways.delete_group(socket.assigns.group, socket.assigns.subject)
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/sites")}
end
+
+ attr :outdated, :boolean
+
+ defp version_status(assigns) do
+ ~H"""
+ <.icon
+ :if={!@outdated}
+ name="hero-check-circle"
+ class="w-4 h-4 text-green-500"
+ title="Up to date"
+ />
+ <.icon
+ :if={@outdated}
+ name="hero-arrow-up-circle"
+ class="w-4 h-4 text-primary-500"
+ title="New version available"
+ />
+ """
+ end
end
diff --git a/elixir/apps/web/lib/web/router.ex b/elixir/apps/web/lib/web/router.ex
index 40d3cdee4..fcbb5ff33 100644
--- a/elixir/apps/web/lib/web/router.ex
+++ b/elixir/apps/web/lib/web/router.ex
@@ -211,6 +211,7 @@ defmodule Web.Router do
scope "/account" do
live "/", Account
live "/edit", Account.Edit
+ live "/notifications/edit", Account.Notifications.Edit
end
live "/billing", Billing
diff --git a/elixir/apps/web/test/web/live/settings/account/notifications_edit_test.exs b/elixir/apps/web/test/web/live/settings/account/notifications_edit_test.exs
new file mode 100644
index 000000000..94364ac80
--- /dev/null
+++ b/elixir/apps/web/test/web/live/settings/account/notifications_edit_test.exs
@@ -0,0 +1,106 @@
+defmodule Web.Live.Settings.Account.NotificationsEditTest do
+ use Web.ConnCase, async: true
+
+ setup do
+ Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
+
+ account =
+ Fixtures.Accounts.create_account(
+ metadata: %{
+ stripe: %{
+ customer_id: "cus_NffrFeUfNV2Hib",
+ subscription_id: "sub_NffrFeUfNV2Hib",
+ product_name: "Enterprise"
+ }
+ },
+ limits: %{
+ monthly_active_users_count: 100
+ }
+ )
+
+ identity = Fixtures.Auth.create_identity(account: account, actor: [type: :account_admin_user])
+
+ %{
+ account: account,
+ identity: identity
+ }
+ end
+
+ test "redirects to sign in page for unauthorized user", %{account: account, conn: conn} do
+ path = ~p"/#{account}/settings/account/notifications/edit"
+
+ assert live(conn, path) ==
+ {:error,
+ {:redirect,
+ %{
+ to: ~p"/#{account}?#{%{redirect_to: path}}",
+ flash: %{"error" => "You must sign in to access this page."}
+ }}}
+ end
+
+ test "renders breadcrumbs item", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ {:ok, _lv, html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/account/notifications/edit")
+
+ assert item = Floki.find(html, "[aria-label='Breadcrumb']")
+ breadcrumbs = String.trim(Floki.text(item))
+ assert breadcrumbs =~ "Account Settings"
+ assert breadcrumbs =~ "Edit Notifications"
+ end
+
+ test "renders enable/disable form", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/account/notifications/edit")
+
+ form = form(lv, "form")
+
+ assert find_inputs(form) == [
+ "account[config][_persistent_id]",
+ "account[config][notifications][_persistent_id]",
+ "account[config][notifications][outdated_gateway][_persistent_id]",
+ "account[config][notifications][outdated_gateway][enabled]"
+ ]
+ end
+
+ test "updates notifications status on valid attrs", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ attrs = %{
+ "config" => %{
+ "_persistent_id" => "0",
+ "notifications" => %{
+ "_persistent_id" => "0",
+ "outdated_gateway" => %{"_persistent_id" => "0", "enabled" => "true"}
+ }
+ }
+ }
+
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/account/notifications/edit")
+
+ lv
+ |> form("form", account: attrs)
+ |> render_submit()
+
+ assert_redirected(lv, ~p"/#{account}/settings/account")
+
+ assert account = Repo.get_by(Domain.Accounts.Account, id: account.id)
+ assert account.config.notifications.outdated_gateway.enabled == true
+ end
+end
diff --git a/elixir/apps/web/test/web/live/settings/account_test.exs b/elixir/apps/web/test/web/live/settings/account_test.exs
index fcfc2628d..d1de50d0d 100644
--- a/elixir/apps/web/test/web/live/settings/account_test.exs
+++ b/elixir/apps/web/test/web/live/settings/account_test.exs
@@ -121,4 +121,18 @@ defmodule Web.Live.Settings.AccountTest do
assert html =~ "This account has been disabled."
assert html =~ "contact support"
end
+
+ test "renders notification settings for account", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/account")
+
+ html = lv |> render()
+ assert html =~ "Gateway Upgrade Available"
+ end
end
diff --git a/elixir/apps/web/test/web/live/settings/dns_test.exs b/elixir/apps/web/test/web/live/settings/dns_test.exs
index 7dc909585..d0a4f21a4 100644
--- a/elixir/apps/web/test/web/live/settings/dns_test.exs
+++ b/elixir/apps/web/test/web/live/settings/dns_test.exs
@@ -40,7 +40,7 @@ defmodule Web.Live.Settings.DNSTest do
assert breadcrumbs =~ "DNS Settings"
end
- test "renders form", %{
+ test "renders form with no input fields", %{
account: account,
identity: identity,
conn: conn
@@ -54,11 +54,47 @@ defmodule Web.Live.Settings.DNSTest do
form = lv |> form("form")
+ assert find_inputs(form) == [
+ "account[config][_persistent_id]",
+ "account[config][clients_upstream_dns_drop][]"
+ ]
+ end
+
+ test "renders input field on button click", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ Fixtures.Accounts.update_account(account, %{config: %{clients_upstream_dns: []}})
+
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/dns")
+
+ attrs = %{
+ "_target" => ["account", "config", "clients_upstream_dns_sort"],
+ "account" => %{
+ "config" => %{
+ "_persistent_id" => "0",
+ "clients_upstream_dns_drop" => [""],
+ "clients_upstream_dns_sort" => ["new"]
+ }
+ }
+ }
+
+ lv
+ |> render_click(:change, attrs)
+
+ form = lv |> form("form")
+
assert find_inputs(form) == [
"account[config][_persistent_id]",
"account[config][clients_upstream_dns][0][_persistent_id]",
"account[config][clients_upstream_dns][0][address]",
- "account[config][clients_upstream_dns][0][protocol]"
+ "account[config][clients_upstream_dns][0][protocol]",
+ "account[config][clients_upstream_dns_drop][]",
+ "account[config][clients_upstream_dns_sort][]"
]
end
@@ -82,6 +118,17 @@ defmodule Web.Live.Settings.DNSTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/settings/dns")
+ lv
+ |> element("form")
+ |> render_change(%{
+ "account" => %{
+ "config" => %{
+ "clients_upstream_dns_drop" => [""],
+ "clients_upstream_dns_sort" => ["new"]
+ }
+ }
+ })
+
lv
|> form("form", attrs)
|> render_submit()
@@ -93,9 +140,8 @@ defmodule Web.Live.Settings.DNSTest do
"account[config][clients_upstream_dns][0][_persistent_id]",
"account[config][clients_upstream_dns][0][address]",
"account[config][clients_upstream_dns][0][protocol]",
- "account[config][clients_upstream_dns][1][_persistent_id]",
- "account[config][clients_upstream_dns][1][address]",
- "account[config][clients_upstream_dns][1][protocol]"
+ "account[config][clients_upstream_dns_drop][]",
+ "account[config][clients_upstream_dns_sort][]"
]
end
@@ -135,7 +181,9 @@ defmodule Web.Live.Settings.DNSTest do
"account[config][clients_upstream_dns][1][protocol]",
"account[config][clients_upstream_dns][2][_persistent_id]",
"account[config][clients_upstream_dns][2][address]",
- "account[config][clients_upstream_dns][2][protocol]"
+ "account[config][clients_upstream_dns][2][protocol]",
+ "account[config][clients_upstream_dns_drop][]",
+ "account[config][clients_upstream_dns_sort][]"
]
end
@@ -190,7 +238,7 @@ defmodule Web.Live.Settings.DNSTest do
|> render_change() =~ "all addresses must be unique"
end
- test "does not display 'cannot be empty' error message", %{
+ test "displays 'cannot be empty' error message", %{
account: account,
identity: identity,
conn: conn
@@ -212,7 +260,7 @@ defmodule Web.Live.Settings.DNSTest do
|> form("form", attrs)
|> render_submit()
- refute lv
+ assert lv
|> form("form", %{
account: %{
config: %{
diff --git a/elixir/apps/web/test/web/live/sites/gateways/index_test.exs b/elixir/apps/web/test/web/live/sites/gateways/index_test.exs
index 540e92628..b8157ba85 100644
--- a/elixir/apps/web/test/web/live/sites/gateways/index_test.exs
+++ b/elixir/apps/web/test/web/live/sites/gateways/index_test.exs
@@ -51,7 +51,12 @@ defmodule Web.Live.Sites.Gateways.IndexTest do
group: group,
conn: conn
} do
- gateway = Fixtures.Gateways.create_gateway(account: account, group: group)
+ gateway =
+ Fixtures.Gateways.create_gateway(
+ account: account,
+ group: group,
+ context: %{user_agent: "iOS/12.5 (iPhone) connlib/1.3.2"}
+ )
{:ok, lv, _html} =
conn
@@ -67,7 +72,8 @@ defmodule Web.Live.Sites.Gateways.IndexTest do
assert row == %{
"instance" => gateway.name,
"remote ip" => to_string(gateway.last_seen_remote_ip),
- "status" => "Offline"
+ "status" => "Offline",
+ "version" => "1.3.2"
}
end
@@ -77,7 +83,12 @@ defmodule Web.Live.Sites.Gateways.IndexTest do
group: group,
conn: conn
} do
- gateway = Fixtures.Gateways.create_gateway(account: account, group: group)
+ gateway =
+ Fixtures.Gateways.create_gateway(
+ account: account,
+ group: group,
+ context: %{user_agent: "iOS/12.5 (iPhone) connlib/1.3.2"}
+ )
{:ok, lv, _html} =
conn
@@ -98,7 +109,8 @@ defmodule Web.Live.Sites.Gateways.IndexTest do
assert row == %{
"instance" => gateway.name,
"remote ip" => to_string(gateway.last_seen_remote_ip),
- "status" => "Online"
+ "status" => "Online",
+ "version" => "1.3.2"
}
end)
end
diff --git a/elixir/apps/web/test/web/live/sites/show_test.exs b/elixir/apps/web/test/web/live/sites/show_test.exs
index a77fff865..8c2feeb55 100644
--- a/elixir/apps/web/test/web/live/sites/show_test.exs
+++ b/elixir/apps/web/test/web/live/sites/show_test.exs
@@ -161,6 +161,7 @@ defmodule Web.Live.Sites.ShowTest do
|> with_table_row("instance", gateway.name, fn row ->
assert gateway.last_seen_remote_ip
assert row["remote ip"] =~ to_string(gateway.last_seen_remote_ip)
+ assert row["version"] =~ gateway.last_seen_version
assert row["status"] =~ "Online"
end)
end
diff --git a/elixir/config/config.exs b/elixir/config/config.exs
index c7bb12943..9e6e44143 100644
--- a/elixir/config/config.exs
+++ b/elixir/config/config.exs
@@ -75,6 +75,17 @@ config :domain, Domain.GoogleCloudPlatform,
sign_endpoint_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/",
cloud_storage_url: "https://storage.googleapis.com"
+config :domain, Domain.ComponentVersions,
+ firezone_releases_url: "https://www.firezone.dev/api/releases",
+ fetch_from_url: true,
+ versions: [
+ apple: "1.3.6",
+ android: "1.3.5",
+ gateway: "1.3.2",
+ gui: "1.3.8",
+ headless: "1.3.4"
+ ]
+
config :domain, Domain.Cluster,
adapter: nil,
adapter_config: []
diff --git a/elixir/config/test.exs b/elixir/config/test.exs
index 2ec4005c9..62cf87b76 100644
--- a/elixir/config/test.exs
+++ b/elixir/config/test.exs
@@ -26,6 +26,16 @@ config :domain, platform_adapter: Domain.GoogleCloudPlatform
config :domain, Domain.GoogleCloudPlatform, service_account_email: "foo@iam.example.com"
+config :domain, Domain.ComponentVersions,
+ fetch_from_url: false,
+ versions: [
+ apple: "1.0.0",
+ android: "1.0.0",
+ gateway: "1.0.0",
+ gui: "1.0.0",
+ headless: "1.0.0"
+ ]
+
config :domain, Domain.Telemetry.GoogleCloudMetricsReporter, project_id: "fz-test"
config :domain, web_external_url: "http://localhost:13100"