mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(portal): Add outdated gateway notifications (#6841)
Why: * Without some type of notification, users do not realize that new Gateway versions have been released and thus do not seem to be upgrading their deployed Gateways.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
defmodule Domain.Accounts do
|
||||
alias Web.Settings.Account
|
||||
alias Domain.{Repo, Config, PubSub}
|
||||
alias Domain.{Auth, Billing}
|
||||
alias Domain.Accounts.{Account, Features, Authorizer}
|
||||
@@ -18,6 +19,18 @@ defmodule Domain.Accounts do
|
||||
end
|
||||
end
|
||||
|
||||
def all_active_paid_accounts_pending_notification! do
|
||||
["Team", "Enterprise"]
|
||||
|> Enum.flat_map(&all_active_accounts_by_subscription_name_pending_notification!/1)
|
||||
end
|
||||
|
||||
def all_active_accounts_by_subscription_name_pending_notification!(subscription_name) do
|
||||
Account.Query.not_disabled()
|
||||
|> Account.Query.by_stripe_product_name(subscription_name)
|
||||
|> 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
|
||||
@@ -138,6 +151,14 @@ defmodule Domain.Accounts do
|
||||
end
|
||||
end
|
||||
|
||||
def type(%Account{metadata: %{stripe: %{product_name: type}}}) do
|
||||
type || "Starter"
|
||||
end
|
||||
|
||||
def type(%Account{}) do
|
||||
"Starter"
|
||||
end
|
||||
|
||||
### PubSub
|
||||
|
||||
defp account_topic(%Account{} = account), do: account_topic(account.id)
|
||||
|
||||
@@ -10,8 +10,7 @@ defmodule Domain.Accounts.Account do
|
||||
# Updated by the billing subscription metadata fields
|
||||
embeds_one :features, Domain.Accounts.Features, on_replace: :delete
|
||||
embeds_one :limits, Domain.Accounts.Limits, on_replace: :delete
|
||||
|
||||
embeds_one :config, Domain.Accounts.Config, on_replace: :delete
|
||||
embeds_one :config, Domain.Accounts.Config, on_replace: :update
|
||||
|
||||
embeds_one :metadata, Metadata, primary_key: false, on_replace: :update do
|
||||
embeds_one :stripe, Stripe, primary_key: false, on_replace: :update do
|
||||
|
||||
@@ -35,6 +35,14 @@ defmodule Domain.Accounts.Account.Query do
|
||||
)
|
||||
end
|
||||
|
||||
def by_stripe_product_name(queryable, account_type) do
|
||||
where(
|
||||
queryable,
|
||||
[accounts: accounts],
|
||||
fragment("?->'stripe'->>'product_name' = ?", accounts.metadata, ^account_type)
|
||||
)
|
||||
end
|
||||
|
||||
def by_slug(queryable, slug) do
|
||||
where(queryable, [accounts: accounts], accounts.slug == ^slug)
|
||||
end
|
||||
@@ -47,6 +55,26 @@ defmodule Domain.Accounts.Account.Query do
|
||||
end
|
||||
end
|
||||
|
||||
def by_notification_last_notified(queryable, notification, hours) do
|
||||
interval = Duration.new!(hour: hours)
|
||||
|
||||
where(
|
||||
queryable,
|
||||
[accounts: accounts],
|
||||
fragment(
|
||||
"(?->'notifications'->?->>'last_notified')::timestamp < NOW() - ?::interval",
|
||||
accounts.config,
|
||||
^notification,
|
||||
^interval
|
||||
) or
|
||||
fragment(
|
||||
"(?->'notifications'->?->>'last_notified') IS NULL",
|
||||
accounts.config,
|
||||
^notification
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# Pagination
|
||||
|
||||
@impl Domain.Repo.Query
|
||||
@@ -98,7 +126,7 @@ defmodule Domain.Accounts.Account.Query do
|
||||
end
|
||||
|
||||
def filter_by_status(queryable, "enabled") do
|
||||
{queryable, dynamic([accounts: accounts], not is_nil(accounts.disabled_at))}
|
||||
{queryable, dynamic([accounts: accounts], is_nil(accounts.disabled_at))}
|
||||
end
|
||||
|
||||
def filter_by_status(queryable, "disabled") do
|
||||
|
||||
@@ -7,6 +7,15 @@ defmodule Domain.Accounts.Config do
|
||||
field :protocol, Ecto.Enum, values: [:ip_port, :dns_over_tls, :dns_over_http]
|
||||
field :address, :string
|
||||
end
|
||||
|
||||
embeds_one :notifications, Notifications,
|
||||
primary_key: false,
|
||||
on_replace: :update do
|
||||
embeds_one :outdated_gateway, Domain.Accounts.Config.Notifications.Email,
|
||||
on_replace: :update
|
||||
|
||||
embeds_one :idp_sync_error, Domain.Accounts.Config.Notifications.Email, on_replace: :update
|
||||
end
|
||||
end
|
||||
|
||||
def supported_dns_protocols, do: ~w[ip_port]a
|
||||
|
||||
@@ -8,7 +8,12 @@ defmodule Domain.Accounts.Config.Changeset do
|
||||
def changeset(config \\ %Config{}, attrs) do
|
||||
config
|
||||
|> cast(attrs, [])
|
||||
|> cast_embed(:clients_upstream_dns, with: &client_upstream_dns_changeset/2)
|
||||
|> cast_embed(:clients_upstream_dns,
|
||||
with: &client_upstream_dns_changeset/2,
|
||||
sort_param: :clients_upstream_dns_sort,
|
||||
drop_param: :clients_upstream_dns_drop
|
||||
)
|
||||
|> cast_embed(:notifications, with: ¬ifications_changeset/2)
|
||||
|> validate_unique_clients_upstream_dns()
|
||||
end
|
||||
|
||||
@@ -76,4 +81,11 @@ defmodule Domain.Accounts.Config.Changeset do
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def notifications_changeset(notifications, attrs) do
|
||||
notifications
|
||||
|> cast(attrs, [])
|
||||
|> cast_embed(:outdated_gateway, with: &Config.Notifications.Email.Changeset.changeset/2)
|
||||
|> cast_embed(:idp_sync_error, with: &Config.Notifications.Email.Changeset.changeset/2)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
defmodule Domain.Accounts.Config.Notifications.Email do
|
||||
use Domain, :schema
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field :enabled, :boolean
|
||||
field :last_notified, :utc_datetime
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule Domain.Accounts.Config.Notifications.Email.Changeset do
|
||||
use Domain, :changeset
|
||||
alias Domain.Accounts.Config.Notifications.Email
|
||||
|
||||
def changeset(config \\ %Email{}, attrs) do
|
||||
config
|
||||
|> cast(attrs, [:enabled, :last_notified])
|
||||
end
|
||||
end
|
||||
@@ -34,6 +34,8 @@ defmodule Domain.Application do
|
||||
Domain.Billing,
|
||||
Domain.Mailer,
|
||||
Domain.Mailer.RateLimiter,
|
||||
Domain.Notifications,
|
||||
Domain.ComponentVersions,
|
||||
|
||||
# Observability
|
||||
Domain.Telemetry
|
||||
|
||||
88
elixir/apps/domain/lib/domain/component_versions.ex
Normal file
88
elixir/apps/domain/lib/domain/component_versions.ex
Normal file
@@ -0,0 +1,88 @@
|
||||
defmodule Domain.ComponentVersions do
|
||||
alias Domain.ComponentVersions
|
||||
use Supervisor
|
||||
require Logger
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__.Supervisor)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
pool_opts = Domain.Config.fetch_env!(:domain, :http_client_ssl_opts)
|
||||
|
||||
fetch_from_url? =
|
||||
Domain.Config.fetch_env!(:domain, ComponentVersions)
|
||||
|> Keyword.get(:fetch_from_url)
|
||||
|
||||
children =
|
||||
[
|
||||
{Finch, name: __MODULE__.Finch, pools: %{default: pool_opts}}
|
||||
]
|
||||
|
||||
children =
|
||||
if fetch_from_url? do
|
||||
children ++ [ComponentVersions.Refresher]
|
||||
else
|
||||
children
|
||||
end
|
||||
|
||||
Supervisor.init(children, strategy: :rest_for_one)
|
||||
end
|
||||
|
||||
def gateway_version do
|
||||
ComponentVersions.component_version(:gateway)
|
||||
end
|
||||
|
||||
def component_version(component) do
|
||||
Domain.Config.get_env(:domain, ComponentVersions, [])
|
||||
|> Keyword.get(:versions, [])
|
||||
|> Keyword.get(component, "0.0.0")
|
||||
end
|
||||
|
||||
def fetch_versions do
|
||||
config = fetch_config!()
|
||||
releases_url = Keyword.fetch!(config, :firezone_releases_url)
|
||||
fetch_from_url? = Keyword.fetch!(config, :fetch_from_url)
|
||||
|
||||
if fetch_from_url? do
|
||||
fetch_versions_from_url(releases_url)
|
||||
else
|
||||
{:ok, Keyword.fetch!(config, :versions)}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_config! do
|
||||
Domain.Config.fetch_env!(:domain, __MODULE__)
|
||||
end
|
||||
|
||||
defp fetch_versions_from_url(releases_url) do
|
||||
request =
|
||||
Finch.build(:get, releases_url, [])
|
||||
|
||||
case Finch.request(request, __MODULE__.Finch) do
|
||||
{:ok, %Finch.Response{status: 200, body: response}} ->
|
||||
versions = decode_versions_response(response)
|
||||
{:ok, Enum.into(versions, [])}
|
||||
|
||||
{:ok, response} ->
|
||||
Logger.error("Can't fetch Firezone versions", reason: inspect(response))
|
||||
{:error, {response.status, response.body}}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Can't fetch Firezone versions", reason: inspect(reason))
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_versions_response(response) do
|
||||
case Jason.decode(response, keys: :atoms) do
|
||||
{:ok, %{apple: _, android: _, gateway: _, gui: _, headless: _} = decoded_json} ->
|
||||
decoded_json
|
||||
|
||||
_ ->
|
||||
fetch_config!()
|
||||
|> Keyword.fetch!(:versions)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
defmodule Domain.ComponentVersions.Refresher do
|
||||
use GenServer
|
||||
require Logger
|
||||
alias Domain.ComponentVersions
|
||||
|
||||
@default_refresh_interval :timer.minutes(1)
|
||||
|
||||
def start_link(opts) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
refresh_interval = Keyword.get(opts, :refresh_interval, @default_refresh_interval)
|
||||
|
||||
{:ok, %{refresh_interval: refresh_interval}, {:continue, :load}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_continue(:load, state) do
|
||||
refresh_versions()
|
||||
:ok = schedule_refresh(state.refresh_interval)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:refresh_versions, state) do
|
||||
refresh_versions()
|
||||
:ok = schedule_refresh(state.refresh_interval)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:force_refesh, state) do
|
||||
refresh_versions()
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp refresh_versions do
|
||||
case ComponentVersions.fetch_versions() do
|
||||
{:ok, versions} ->
|
||||
new_config =
|
||||
Domain.Config.get_env(:domain, ComponentVersions)
|
||||
|> Keyword.merge(versions: versions)
|
||||
|
||||
Application.put_env(:domain, ComponentVersions, new_config)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.debug("Error fetching component versions: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp schedule_refresh(refresh_interval) do
|
||||
Process.send_after(self(), :refresh_versions, refresh_interval)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,6 @@
|
||||
defmodule Domain.Gateways do
|
||||
use Supervisor
|
||||
alias Domain.Accounts.Account
|
||||
alias Domain.{Repo, Auth, Geo, PubSub}
|
||||
alias Domain.{Accounts, Resources, Tokens, Billing}
|
||||
alias Domain.Gateways.{Authorizer, Gateway, Group, Presence}
|
||||
@@ -49,6 +50,12 @@ defmodule Domain.Gateways do
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def all_groups_for_account!(%Accounts.Account{} = account) do
|
||||
Group.Query.not_deleted()
|
||||
|> Group.Query.by_account_id(account.id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def new_group(attrs \\ %{}) do
|
||||
change_group(%Group{}, attrs)
|
||||
end
|
||||
@@ -189,6 +196,15 @@ defmodule Domain.Gateways do
|
||||
end
|
||||
end
|
||||
|
||||
def all_gateways_for_account!(%Account{} = account, opts \\ []) do
|
||||
{preload, _opts} = Keyword.pop(opts, :preload, [])
|
||||
|
||||
Gateway.Query.not_deleted()
|
||||
|> Gateway.Query.by_account_id(account.id)
|
||||
|> Repo.all()
|
||||
|> Repo.preload(preload)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def preload_gateways_presence([gateway]) do
|
||||
gateway.account_id
|
||||
@@ -405,6 +421,15 @@ defmodule Domain.Gateways do
|
||||
end
|
||||
end
|
||||
|
||||
def gateway_outdated?(gateway) do
|
||||
latest_release = Domain.ComponentVersions.gateway_version()
|
||||
|
||||
case Version.compare(gateway.last_seen_version, latest_release) do
|
||||
:lt -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
### Presence
|
||||
|
||||
def account_gateways_presence_topic(account_or_id),
|
||||
|
||||
19
elixir/apps/domain/lib/domain/mailer/notifications.ex
Normal file
19
elixir/apps/domain/lib/domain/mailer/notifications.ex
Normal file
@@ -0,0 +1,19 @@
|
||||
defmodule Domain.Mailer.Notifications do
|
||||
import Swoosh.Email
|
||||
import Domain.Mailer
|
||||
import Phoenix.Template, only: [embed_templates: 2]
|
||||
|
||||
embed_templates "notifications/*.html", suffix: "_html"
|
||||
embed_templates "notifications/*.text", suffix: "_text"
|
||||
|
||||
def outdated_gateway_email(account, gateways, email) do
|
||||
default_email()
|
||||
|> subject("Firezone Gateway Upgrade Available")
|
||||
|> to(email)
|
||||
|> render_body(__MODULE__, :outdated_gateway,
|
||||
account: account,
|
||||
gateways: gateways,
|
||||
latest_version: Domain.ComponentVersions.gateway_version()
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<style>
|
||||
td,th,div,p,a,h1,h2,h3,h4,h5,h6 {font-family: "Segoe UI", sans-serif; mso-line-height-rule: exactly;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<title>Firezone Gateway Update Available</title>
|
||||
<style>
|
||||
.hover-important-text-decoration-underline:hover {
|
||||
text-decoration: underline !important
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.sm-my-8 {
|
||||
margin-top: 32px !important;
|
||||
margin-bottom: 32px !important
|
||||
}
|
||||
.sm-px-4 {
|
||||
padding-left: 16px !important;
|
||||
padding-right: 16px !important
|
||||
}
|
||||
.sm-px-6 {
|
||||
padding-left: 24px !important;
|
||||
padding-right: 24px !important
|
||||
}
|
||||
.sm-leading-8 {
|
||||
line-height: 32px !important
|
||||
}
|
||||
}
|
||||
.rtl-text-right:where([dir="rtl"], [dir="rtl"] *) {
|
||||
text-align: right !important
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; width: 100%; background-color: #f6f6f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word">
|
||||
<div style="display: none">
|
||||
Firezone Gateway Update Available
|
||||
</div>
|
||||
<div role="article" aria-roledescription="email" aria-label="Firezone Gateway Update Available" lang="en">
|
||||
<div class="sm-px-4" style="background-color: #f6f6f6; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif">
|
||||
<table align="center" cellpadding="0" cellspacing="0" role="none">
|
||||
<tr>
|
||||
<td style="width: 552px; max-width: 100%">
|
||||
<div class="sm-my-8" style="margin-top: 48px; margin-bottom: 48px; text-align: center">
|
||||
<a href="https://firezone.dev">
|
||||
<div>
|
||||
<img src="https://www.firezone.dev/images/logo-lockup.png" width="250" alt="Firezone logo" style="max-width: 100%; vertical-align: middle; line-height: 1">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="none">
|
||||
<tr>
|
||||
<td class="sm-px-6" style="border-radius: 4px; background-color: #ffffff; padding: 48px; font-size: 16px; color: #4f4f4f; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)">
|
||||
<h1 class="sm-leading-8" style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #000000">
|
||||
Gateway Update Available!
|
||||
</h1>
|
||||
<p style="margin: 0; line-height: 24px">
|
||||
The latest Firezone Gateway release is: <%= @latest_version %>
|
||||
</p>
|
||||
<div role="separator" style="line-height: 16px">‍</div>
|
||||
<p style="margin: 0; line-height: 24px;">
|
||||
The following list of Gateways in your Firezone Account can be updated.
|
||||
</p>
|
||||
<div role="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0">‍</div>
|
||||
<p style="margin: 0; line-height: 24px;">
|
||||
<span style="margin-bottom: 32px; font-weight: 500; text-decoration-line: underline; text-underline-offset: 2px">Outdated Gateways</span>
|
||||
</p>
|
||||
<table class="rtl-text-right" style="width: 100%; text-align: left; font-size: 14px; color: #6d6d6d" cellpadding="0" cellspacing="0" role="none">
|
||||
<%= for gateway <- @gateways do %>
|
||||
<tr style="border-bottom-width: 1px; background-color: #ffffff">
|
||||
<th scope="row" style="white-space: nowrap; padding-left: 24px; padding-right: 24px; font-weight: 500; color: #3d3d3d"><%= gateway.name %></th>
|
||||
<td style="padding: 4px 24px"><%= gateway.last_seen_version %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
<p></p>
|
||||
<div role="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;">‍</div>
|
||||
<p style="margin: 0; line-height: 24px;">
|
||||
We recommend updating the Gateways at your earliest convenience. Help on how to update Gateways can be found
|
||||
in our <a href="https://www.firezone.dev/kb/administer/upgrading">Firezone Gateway Upgrade Guide</a>
|
||||
<br>
|
||||
<br>
|
||||
Thanks, <br>The Firezone Team
|
||||
</p>
|
||||
<div role="separator" style="line-height: 16px">‍</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr role="separator">
|
||||
<td style="line-height: 48px">‍</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
|
||||
<p style="margin: 0; font-style: italic">
|
||||
Blazing-fast alternative to legacy VPNs
|
||||
</p>
|
||||
<p style="cursor: default">
|
||||
<a href="https://www.firezone.dev/kb" class="hover-important-text-decoration-underline" style="color: #37007f; text-decoration: none">Docs</a>
|
||||
•
|
||||
<a href="https://github.com/firezone" class="hover-important-text-decoration-underline" style="color: #37007f; text-decoration: none;">Github</a>
|
||||
•
|
||||
<a href="https://x.com/firezonehq" class="hover-important-text-decoration-underline" style="color: #37007f; text-decoration: none;">X</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
Gateway Update Available!
|
||||
|
||||
The latest Firezone Gateway release is: <%= @latest_version %>
|
||||
|
||||
The following list of Gateways in your Firezone Account can be updated:
|
||||
<%= for gateway <- @gateways do %>
|
||||
<%= "- #{gateway.name} -> #{gateway.last_seen_version}" %>
|
||||
<% end %>
|
||||
|
||||
We recommend updating the Gateways at your earliest convenience.
|
||||
|
||||
Help on how to update Gateways can be found in our Firezone Documentation:
|
||||
https://www.firezone.dev/kb/administer/upgrading
|
||||
|
||||
Thanks,
|
||||
The Firezone Team
|
||||
17
elixir/apps/domain/lib/domain/notifications.ex
Normal file
17
elixir/apps/domain/lib/domain/notifications.ex
Normal file
@@ -0,0 +1,17 @@
|
||||
defmodule Domain.Notifications do
|
||||
use Supervisor
|
||||
require Logger
|
||||
|
||||
def start_link(_init_arg) do
|
||||
Supervisor.start_link(__MODULE__, nil, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_init_arg) do
|
||||
children = [
|
||||
Domain.Notifications.Jobs.OutdatedGateways
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
defmodule Domain.Notifications.Jobs.OutdatedGateways do
|
||||
use Domain.Jobs.Job,
|
||||
otp_app: :domain,
|
||||
every: :timer.minutes(5),
|
||||
executor: Domain.Jobs.Executors.GloballyUnique
|
||||
|
||||
require Logger
|
||||
require OpenTelemetry.Tracer
|
||||
|
||||
alias Domain.Actors
|
||||
alias Domain.{Accounts, Gateways, Mailer}
|
||||
|
||||
@impl true
|
||||
if Mix.env() == :prod do
|
||||
def execute(_config) do
|
||||
# Should only run on Sundays
|
||||
day_of_week = Date.utc_today() |> Date.day_of_week()
|
||||
if day_of_week == 7, do: run_check()
|
||||
end
|
||||
else
|
||||
def execute(_config) do
|
||||
run_check()
|
||||
end
|
||||
end
|
||||
|
||||
defp run_check do
|
||||
Accounts.all_active_paid_accounts_pending_notification!()
|
||||
|> Enum.each(fn account ->
|
||||
all_online_gateways_for_account(account)
|
||||
|> Enum.filter(&Gateways.gateway_outdated?/1)
|
||||
|> send_notifications(account)
|
||||
end)
|
||||
end
|
||||
|
||||
defp all_online_gateways_for_account(account) do
|
||||
gateways_by_id =
|
||||
Gateways.all_gateways_for_account!(account)
|
||||
|> Enum.group_by(& &1.id)
|
||||
|
||||
Gateways.all_groups_for_account!(account)
|
||||
|> Enum.flat_map(&Gateways.all_online_gateway_ids_by_group_id!(&1.id))
|
||||
|> Enum.flat_map(&Map.get(gateways_by_id, &1))
|
||||
end
|
||||
|
||||
defp send_notifications([], _account) do
|
||||
Logger.debug("No outdated gateways for account")
|
||||
end
|
||||
|
||||
defp send_notifications(gateways, account) do
|
||||
Domain.Actors.all_admins_for_account!(account, preload: :identities)
|
||||
|> Enum.flat_map(&list_emails_for_actor/1)
|
||||
|> Enum.each(&send_email(account, gateways, &1))
|
||||
|
||||
Domain.Accounts.update_account(account, %{
|
||||
config: %{
|
||||
notifications: %{
|
||||
outdated_gateway: %{
|
||||
last_notified: DateTime.utc_now()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defp list_emails_for_actor(%Actors.Actor{} = actor) do
|
||||
actor.identities
|
||||
|> Enum.map(&Domain.Auth.get_identity_email/1)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp send_email(account, gateways, email) do
|
||||
Mailer.Notifications.outdated_gateway_email(account, gateways, email)
|
||||
|> Mailer.deliver_with_rate_limit()
|
||||
end
|
||||
|
||||
def notified_in_last_24h?(%Accounts.Account{} = account) do
|
||||
last_notification = last_notified(account.config.notifications)
|
||||
|
||||
if is_nil(last_notification) do
|
||||
false
|
||||
else
|
||||
DateTime.diff(DateTime.utc_now(), last_notification, :hour) < 24
|
||||
end
|
||||
end
|
||||
|
||||
defp last_notified(%{outdated_gateway: %{last_notified: datetime}}), do: datetime
|
||||
defp last_notified(_), do: nil
|
||||
end
|
||||
@@ -41,6 +41,110 @@ defmodule Domain.AccountsTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "all_active_paid_accounts_pending_notification!/0" do
|
||||
test "returns paid and active accounts" do
|
||||
Fixtures.Accounts.create_account()
|
||||
Fixtures.Accounts.create_account() |> Fixtures.Accounts.change_to_enterprise()
|
||||
Fixtures.Accounts.create_account() |> Fixtures.Accounts.change_to_team()
|
||||
|
||||
Fixtures.Accounts.create_account()
|
||||
|> Fixtures.Accounts.change_to_team()
|
||||
|> Fixtures.Accounts.disable_account()
|
||||
|
||||
accounts = all_active_paid_accounts_pending_notification!()
|
||||
assert length(accounts) == 2
|
||||
end
|
||||
|
||||
test "returns empty list when no paid accounts exist" do
|
||||
Fixtures.Accounts.create_account()
|
||||
Fixtures.Accounts.create_account() |> Fixtures.Accounts.change_to_starter()
|
||||
|
||||
accounts = all_active_paid_accounts_pending_notification!()
|
||||
assert length(accounts) == 0
|
||||
end
|
||||
|
||||
test "does not return accounts that have been notified within 24 hours" do
|
||||
attrs = %{
|
||||
config: %{
|
||||
notifications: %{
|
||||
outdated_gateway: %{
|
||||
enabled: true,
|
||||
last_notified: DateTime.utc_now() |> DateTime.add(-(60 * 60 * 12))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Fixtures.Accounts.create_account()
|
||||
|
||||
Fixtures.Accounts.create_account(attrs)
|
||||
|> Fixtures.Accounts.change_to_enterprise()
|
||||
|
||||
Fixtures.Accounts.create_account(attrs)
|
||||
|> Fixtures.Accounts.change_to_team()
|
||||
|
||||
Fixtures.Accounts.create_account(attrs)
|
||||
|> Fixtures.Accounts.change_to_team()
|
||||
|> Fixtures.Accounts.disable_account()
|
||||
|
||||
accounts = all_active_paid_accounts_pending_notification!()
|
||||
assert length(accounts) == 0
|
||||
end
|
||||
|
||||
test "returns accounts that have been notified more than 24 hours ago" do
|
||||
attrs = %{
|
||||
config: %{
|
||||
notifications: %{
|
||||
outdated_gateway: %{
|
||||
enabled: true,
|
||||
last_notified: DateTime.utc_now() |> DateTime.add(-(60 * 60 * 36))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Fixtures.Accounts.create_account()
|
||||
|
||||
Fixtures.Accounts.create_account(attrs)
|
||||
|> Fixtures.Accounts.change_to_enterprise()
|
||||
|
||||
Fixtures.Accounts.create_account(attrs)
|
||||
|> Fixtures.Accounts.change_to_team()
|
||||
|
||||
Fixtures.Accounts.create_account(attrs)
|
||||
|> Fixtures.Accounts.change_to_team()
|
||||
|> Fixtures.Accounts.disable_account()
|
||||
|
||||
accounts = all_active_paid_accounts_pending_notification!()
|
||||
assert length(accounts) == 2
|
||||
end
|
||||
end
|
||||
|
||||
describe "all_active_accounts_by_subscription_name_pending_notification!/1" do
|
||||
test "returns all active accounts for given subscription name" do
|
||||
Fixtures.Accounts.create_account()
|
||||
Fixtures.Accounts.create_account() |> Fixtures.Accounts.change_to_team()
|
||||
|
||||
Fixtures.Accounts.create_account()
|
||||
|> Fixtures.Accounts.change_to_team()
|
||||
|> Fixtures.Accounts.disable_account()
|
||||
|
||||
accounts = all_active_accounts_by_subscription_name_pending_notification!("Team")
|
||||
assert length(accounts) == 1
|
||||
end
|
||||
|
||||
test "returns an empty list when no active accounts with type" do
|
||||
Fixtures.Accounts.create_account()
|
||||
|
||||
Fixtures.Accounts.create_account()
|
||||
|> Fixtures.Accounts.change_to_team()
|
||||
|> Fixtures.Accounts.disable_account()
|
||||
|
||||
accounts = all_active_accounts_by_subscription_name_pending_notification!("Team")
|
||||
assert length(accounts) == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_account_by_id/3" do
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
|
||||
54
elixir/apps/domain/test/domain/component_versions_test.exs
Normal file
54
elixir/apps/domain/test/domain/component_versions_test.exs
Normal file
@@ -0,0 +1,54 @@
|
||||
defmodule Domain.ComponentVersionsTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Domain.ComponentVersions
|
||||
alias Domain.ComponentVersions
|
||||
alias Domain.Mocks.FirezoneWebsite
|
||||
|
||||
setup do
|
||||
bypass = Bypass.open()
|
||||
%{bypass: bypass}
|
||||
end
|
||||
|
||||
describe "fetch_versions/0" do
|
||||
test "fetches versions from url", %{bypass: bypass} do
|
||||
versions = %{
|
||||
apple: "1.1.1",
|
||||
android: "1.1.1",
|
||||
gateway: "1.2.3",
|
||||
gui: "1.1.1",
|
||||
headless: "1.1.1"
|
||||
}
|
||||
|
||||
FirezoneWebsite.mock_versions_endpoint(bypass, versions)
|
||||
|
||||
new_config =
|
||||
Domain.Config.get_env(:domain, ComponentVersions)
|
||||
|> Keyword.merge(
|
||||
fetch_from_url: true,
|
||||
firezone_releases_url: "http://localhost:#{bypass.port}/api/releases"
|
||||
)
|
||||
|
||||
Domain.Config.put_env_override(ComponentVersions, new_config)
|
||||
|
||||
assert fetch_versions() == {:ok, Enum.into(versions, [])}
|
||||
end
|
||||
|
||||
test "fetches versions from config" do
|
||||
versions = %{
|
||||
apple: "2.1.1",
|
||||
android: "2.1.1",
|
||||
gateway: "2.2.3",
|
||||
gui: "2.1.1",
|
||||
headless: "2.1.1"
|
||||
}
|
||||
|
||||
new_config =
|
||||
Domain.Config.get_env(:domain, ComponentVersions)
|
||||
|> Keyword.merge(versions: Enum.into(versions, []))
|
||||
|
||||
Domain.Config.put_env_override(ComponentVersions, new_config)
|
||||
|
||||
assert fetch_versions() == {:ok, Enum.into(versions, [])}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -142,6 +142,19 @@ defmodule Domain.GatewaysTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "all_groups_for_account!/1" do
|
||||
test "returns groups for given account", %{account: account} do
|
||||
Fixtures.Gateways.create_group(account: account)
|
||||
Fixtures.Gateways.create_group(account: account)
|
||||
external_group = Fixtures.Gateways.create_group(account: Fixtures.Accounts.create_account())
|
||||
|
||||
groups = all_groups_for_account!(account)
|
||||
|
||||
assert length(groups) == 2
|
||||
refute Enum.any?(groups, &(&1.id == external_group.id))
|
||||
end
|
||||
end
|
||||
|
||||
describe "new_group/0" do
|
||||
test "returns group changeset" do
|
||||
assert %Ecto.Changeset{data: %Gateways.Group{}, changes: changes} = new_group()
|
||||
|
||||
45
elixir/apps/domain/test/domain/mailer/notifications_test.exs
Normal file
45
elixir/apps/domain/test/domain/mailer/notifications_test.exs
Normal file
@@ -0,0 +1,45 @@
|
||||
defmodule Domain.Mailer.NotificationsTest do
|
||||
alias Domain.ComponentVersions
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Mailer.Notifications
|
||||
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
|
||||
%{
|
||||
account: account
|
||||
}
|
||||
end
|
||||
|
||||
describe "outdated_gateway_email/3" do
|
||||
test "should contain current gateway version and list of outdated gateways", %{
|
||||
account: account
|
||||
} do
|
||||
admin_email = "admin@foo.local"
|
||||
|
||||
gateway_1 = Fixtures.Gateways.create_gateway(account: account)
|
||||
gateway_2 = Fixtures.Gateways.create_gateway(account: account)
|
||||
|
||||
current_version = "3.2.1"
|
||||
set_current_version(current_version)
|
||||
|
||||
email_body = outdated_gateway_email(account, [gateway_1, gateway_2], admin_email)
|
||||
|
||||
assert email_body.text_body =~ "The latest Firezone Gateway release is: #{current_version}"
|
||||
assert email_body.text_body =~ gateway_1.name
|
||||
assert email_body.text_body =~ gateway_2.name
|
||||
end
|
||||
end
|
||||
|
||||
defp set_current_version(version) do
|
||||
config = Domain.Config.get_env(:domain, ComponentVersions)
|
||||
|
||||
new_versions =
|
||||
config
|
||||
|> Keyword.get(:versions)
|
||||
|> Keyword.merge(gateway: version)
|
||||
|
||||
new_config = Keyword.merge(config, versions: new_versions)
|
||||
Domain.Config.put_env_override(:domain, ComponentVersions, new_config)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
defmodule Domain.Notifications.Jobs.OutdatedGatewaysTest do
|
||||
alias Domain.ComponentVersions
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Notifications.Jobs.OutdatedGateways
|
||||
|
||||
describe "execute/1" do
|
||||
setup do
|
||||
account =
|
||||
Fixtures.Accounts.create_account()
|
||||
|> Fixtures.Accounts.change_to_enterprise()
|
||||
|
||||
gateway_group = Fixtures.Gateways.create_group(account: account)
|
||||
|
||||
%{
|
||||
account: account,
|
||||
gateway_group: gateway_group
|
||||
}
|
||||
end
|
||||
|
||||
test "sends notification for outdated gateways", %{
|
||||
account: account,
|
||||
gateway_group: gateway_group
|
||||
} do
|
||||
# Create Gateway
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account, group: gateway_group)
|
||||
version = gateway.last_seen_version
|
||||
|
||||
# Set ComponentVersions
|
||||
new_version = bump_version(version)
|
||||
new_config = update_component_versions_config(gateway: new_version)
|
||||
Domain.Config.put_env_override(ComponentVersions, new_config)
|
||||
|
||||
:ok = Domain.Gateways.subscribe_to_gateways_presence_in_group(gateway_group)
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
assert_receive %Phoenix.Socket.Broadcast{topic: "presences:group_gateways:" <> _}
|
||||
|
||||
assert execute(%{}) == :ok
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
assert email.subject == "Firezone Gateway Upgrade Available"
|
||||
assert email.text_body =~ "The latest Firezone Gateway release is: #{new_version}"
|
||||
end)
|
||||
end
|
||||
|
||||
test "does not send notification if gateway up to date", %{
|
||||
account: account,
|
||||
gateway_group: gateway_group
|
||||
} do
|
||||
# Create Gateway
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account, group: gateway_group)
|
||||
version = gateway.last_seen_version
|
||||
|
||||
# Set ComponentVersions
|
||||
new_config = update_component_versions_config(gateway: version)
|
||||
Domain.Config.put_env_override(ComponentVersions, new_config)
|
||||
|
||||
:ok = Domain.Gateways.subscribe_to_gateways_presence_in_group(gateway_group)
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
assert_receive %Phoenix.Socket.Broadcast{topic: "presences:group_gateways:" <> _}
|
||||
|
||||
assert execute(%{}) == :ok
|
||||
refute_email_sent()
|
||||
end
|
||||
end
|
||||
|
||||
defp bump_version(version) do
|
||||
{:ok, current_version} = Version.parse(version)
|
||||
new_minor = current_version.minor + 1
|
||||
"#{current_version.major}.#{new_minor}.0"
|
||||
end
|
||||
|
||||
defp update_component_versions_config(versions) do
|
||||
config = Domain.Config.get_env(:domain, Domain.ComponentVersions)
|
||||
|
||||
new_versions =
|
||||
Keyword.get(config, :versions)
|
||||
|> Keyword.merge(versions)
|
||||
|
||||
Keyword.merge(config, versions: new_versions)
|
||||
end
|
||||
end
|
||||
@@ -49,6 +49,18 @@ defmodule Domain.Fixtures.Accounts do
|
||||
update_account(account, disabled_at: DateTime.utc_now())
|
||||
end
|
||||
|
||||
def change_to_enterprise(%Accounts.Account{} = account) do
|
||||
update_account(account, %{metadata: %{stripe: %{product_name: "Enterprise"}}})
|
||||
end
|
||||
|
||||
def change_to_team(%Accounts.Account{} = account) do
|
||||
update_account(account, %{metadata: %{stripe: %{product_name: "Team"}}})
|
||||
end
|
||||
|
||||
def change_to_starter(%Accounts.Account{} = account) do
|
||||
update_account(account, %{metadata: %{stripe: %{product_name: "Starter"}}})
|
||||
end
|
||||
|
||||
def update_account(account, attrs \\ %{}) do
|
||||
account
|
||||
|> Ecto.Changeset.change(attrs)
|
||||
|
||||
12
elixir/apps/domain/test/support/mocks/firezone_website.ex
Normal file
12
elixir/apps/domain/test/support/mocks/firezone_website.ex
Normal file
@@ -0,0 +1,12 @@
|
||||
defmodule Domain.Mocks.FirezoneWebsite do
|
||||
def mock_versions_endpoint(bypass, versions \\ %{}) do
|
||||
versions_path = "/api/releases"
|
||||
test_pid = self()
|
||||
|
||||
Bypass.stub(bypass, "GET", versions_path, fn conn ->
|
||||
conn = Plug.Conn.fetch_query_params(conn)
|
||||
send(test_pid, {:bypass_request, conn})
|
||||
Plug.Conn.send_resp(conn, 200, Jason.encode!(versions))
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -1206,10 +1206,14 @@ defmodule Web.CoreComponents do
|
||||
@doc """
|
||||
Translates the errors for a field from a keyword list of errors.
|
||||
"""
|
||||
def translate_errors(errors, field) when is_list(errors) do
|
||||
def translate_errors(errors, field) when is_list(errors) or is_map(errors) do
|
||||
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
|
||||
end
|
||||
|
||||
def translate_errors(errors, _field) when is_nil(errors) do
|
||||
[]
|
||||
end
|
||||
|
||||
@doc """
|
||||
This component is meant to be used for step by step instructions
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ defmodule Web.Settings.Account do
|
||||
def mount(_params, _session, socket) do
|
||||
socket =
|
||||
assign(socket,
|
||||
page_title: "Account"
|
||||
page_title: "Account",
|
||||
account_type: Accounts.type(socket.assigns.account)
|
||||
)
|
||||
|
||||
{:ok, socket}
|
||||
@@ -46,6 +47,26 @@ defmodule Web.Settings.Account do
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<:title>
|
||||
Notifications
|
||||
</:title>
|
||||
<:action>
|
||||
<.edit_button
|
||||
:if={@account_type != "Starter"}
|
||||
navigate={~p"/#{@account}/settings/account/notifications/edit"}
|
||||
>
|
||||
Edit Notifications
|
||||
</.edit_button>
|
||||
<.upgrade_badge :if={@account_type == "Starter"} account={@account} />
|
||||
</:action>
|
||||
<:content>
|
||||
<div class="relative overflow-x-auto">
|
||||
<.notifications_table notifications={@account.config.notifications} />
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<:title>
|
||||
Danger zone
|
||||
@@ -66,4 +87,49 @@ defmodule Web.Settings.Account do
|
||||
</.section>
|
||||
"""
|
||||
end
|
||||
|
||||
defp notifications_table(assigns) do
|
||||
~H"""
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-neutral-500">
|
||||
<thead class="text-xs text-neutral-700 uppercase bg-neutral-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-medium">Notification Type</th>
|
||||
<th class="px-4 py-3 font-medium">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-3">
|
||||
Gateway Upgrade Available
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.notification_badge notification={
|
||||
Map.get(@notifications || %{}, :outdated_gateway, %{enabled: false})
|
||||
} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
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
|
||||
</.badge>
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
|
||||
defp notification_badge(assigns) do
|
||||
~H"""
|
||||
<.badge type={if @notification.enabled, do: "success", else: "neutral"}>
|
||||
<%= if @notification.enabled, do: "Enabled", else: "Disabled" %>
|
||||
</.badge>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
defmodule Web.Settings.Account.Notifications.Edit do
|
||||
use Web, :live_view
|
||||
alias Domain.Accounts
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
socket =
|
||||
assign(socket,
|
||||
page_title: "Edit Notifications",
|
||||
form: to_form(Accounts.change_account(socket.assigns.account))
|
||||
)
|
||||
|
||||
{:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.breadcrumbs account={@account}>
|
||||
<.breadcrumb path={~p"/#{@account}/settings/account"}>
|
||||
Account Settings
|
||||
</.breadcrumb>
|
||||
<.breadcrumb path={~p"/#{@account}/settings/account/edit"}>
|
||||
Edit Notifications
|
||||
</.breadcrumb>
|
||||
</.breadcrumbs>
|
||||
<.section>
|
||||
<:title>
|
||||
Edit Notifications
|
||||
</:title>
|
||||
<:content>
|
||||
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
|
||||
<h2 class="mb-4 text-xl text-neutral-900">Edit account notifications</h2>
|
||||
<.form for={@form} phx-change={:change} phx-submit={:submit}>
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
|
||||
<.inputs_for :let={config} field={@form[:config]}>
|
||||
<.inputs_for :let={notifications} field={config[:notifications]}>
|
||||
<table class="w-full text-sm text-left text-neutral-500">
|
||||
<thead class="text-xs text-neutral-700 uppercase bg-neutral-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 font-medium">
|
||||
Notification
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-center">
|
||||
Enable
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-center">
|
||||
Disable
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="bg-white border-b">
|
||||
<.inputs_for :let={outdated_gateway} field={notifications[:outdated_gateway]}>
|
||||
<td scope="row" class="px-6 py-4 whitespace-nowrap">
|
||||
Outdated Gateways
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<.input
|
||||
class="mx-auto"
|
||||
type="radio"
|
||||
field={outdated_gateway[:enabled]}
|
||||
value="true"
|
||||
checked={outdated_gateway[:enabled].value == true}
|
||||
/>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<.input
|
||||
class="mx-auto"
|
||||
type="radio"
|
||||
field={outdated_gateway[:enabled]}
|
||||
value="false"
|
||||
checked={outdated_gateway[:enabled].value != true}
|
||||
/>
|
||||
</td>
|
||||
</.inputs_for>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</.inputs_for>
|
||||
</.inputs_for>
|
||||
</div>
|
||||
<.submit_button>
|
||||
Save
|
||||
</.submit_button>
|
||||
</.form>
|
||||
</div>
|
||||
</:content>
|
||||
</.section>
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_event("change", %{"account" => attrs}, socket) do
|
||||
account = socket.assigns.account
|
||||
|
||||
changeset =
|
||||
Accounts.change_account(account, attrs)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
socket = assign(socket, form: to_form(changeset))
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("submit", %{"account" => attrs}, socket) do
|
||||
with {:ok, account} <-
|
||||
Accounts.update_account(socket.assigns.account, attrs, socket.assigns.subject) do
|
||||
{:noreply,
|
||||
push_navigate(socket,
|
||||
to: ~p"/#{account}/settings/account"
|
||||
)}
|
||||
else
|
||||
{:error, changeset} ->
|
||||
changeset = changeset |> Map.put(:action, :validate)
|
||||
{:noreply, assign(socket, form: to_form(changeset))}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,7 +7,6 @@ defmodule Web.Settings.DNS do
|
||||
Accounts.fetch_account_by_id(socket.assigns.account.id, socket.assigns.subject) do
|
||||
form =
|
||||
Accounts.change_account(account, %{})
|
||||
|> maybe_append_empty_embed()
|
||||
|> to_form()
|
||||
|
||||
socket =
|
||||
@@ -58,8 +57,14 @@ defmodule Web.Settings.DNS do
|
||||
<div>
|
||||
<.inputs_for :let={config} field={@form[:config]}>
|
||||
<.inputs_for :let={dns} field={config[:clients_upstream_dns]}>
|
||||
<input
|
||||
type="hidden"
|
||||
name={"#{config.name}[clients_upstream_dns_sort][]"}
|
||||
value={dns.index}
|
||||
/>
|
||||
|
||||
<div class="flex gap-4 items-start mb-2">
|
||||
<div class="w-1/4">
|
||||
<div class="w-3/12">
|
||||
<.input
|
||||
type="select"
|
||||
label="Protocol"
|
||||
@@ -69,21 +74,42 @@ defmodule Web.Settings.DNS do
|
||||
value={dns[:protocol].value}
|
||||
/>
|
||||
</div>
|
||||
<div class="w-3/4">
|
||||
<div class="w-8/12">
|
||||
<.input
|
||||
label="Address"
|
||||
field={dns[:address]}
|
||||
placeholder="DNS Server Address"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-1/12 flex">
|
||||
<div class="pt-7">
|
||||
<button
|
||||
type="button"
|
||||
name={"#{config.name}[clients_upstream_dns_drop][]"}
|
||||
value={dns.index}
|
||||
phx-click={JS.dispatch("change")}
|
||||
>
|
||||
<.icon name="hero-trash" class="text-red-500 w-6 h-6 relative top-2" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.inputs_for>
|
||||
<% errors =
|
||||
translate_errors(
|
||||
@form.source.changes.config.errors,
|
||||
:clients_upstream_dns
|
||||
) %>
|
||||
<.error :for={error <- errors} data-validation-error-for="clients_upstream_dns">
|
||||
<input type="hidden" name={"#{config.name}[clients_upstream_dns_drop][]"} />
|
||||
<.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
|
||||
</.button>
|
||||
<.error
|
||||
:for={error <- dns_config_errors(@form.source.changes)}
|
||||
data-validation-error-for="clients_upstream_dns"
|
||||
>
|
||||
<%= error %>
|
||||
</.error>
|
||||
</.inputs_for>
|
||||
@@ -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
|
||||
|
||||
@@ -84,11 +84,14 @@ defmodule Web.Sites.Gateways.Index do
|
||||
<%= gateway.name %>
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={gateway} label="remote iP">
|
||||
<:col :let={gateway} label="remote ip">
|
||||
<code>
|
||||
<%= gateway.last_seen_remote_ip %>
|
||||
</code>
|
||||
</:col>
|
||||
<:col :let={gateway} label="version">
|
||||
<%= gateway.last_seen_version %>
|
||||
</:col>
|
||||
<:col :let={gateway} label="status">
|
||||
<.connection_status schema={gateway} />
|
||||
</:col>
|
||||
|
||||
@@ -180,6 +180,10 @@ defmodule Web.Sites.Show do
|
||||
<%= gateway.last_seen_remote_ip %>
|
||||
</code>
|
||||
</:col>
|
||||
<:col :let={gateway} label="version">
|
||||
<.version_status outdated={Gateways.gateway_outdated?(gateway)} />
|
||||
<%= gateway.last_seen_version %>
|
||||
</:col>
|
||||
<:col :let={gateway} label="status">
|
||||
<.connection_status schema={gateway} />
|
||||
</:col>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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: %{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user