diff --git a/elixir/apps/web/lib/web/endpoint.ex b/elixir/apps/web/lib/web/endpoint.ex
index 7ab9e3e11..29f2d4c94 100644
--- a/elixir/apps/web/lib/web/endpoint.ex
+++ b/elixir/apps/web/lib/web/endpoint.ex
@@ -9,6 +9,7 @@ defmodule Web.Endpoint do
plug Plug.RewriteOn, [:x_forwarded_host, :x_forwarded_port, :x_forwarded_proto]
plug Plug.MethodOverride
+ plug :put_hsts_header
plug Web.Plugs.SecureHeaders
plug RemoteIp,
@@ -63,6 +64,22 @@ defmodule Web.Endpoint do
plug Web.Router
+ def put_hsts_header(conn, _opts) do
+ scheme =
+ config(:url, [])
+ |> Keyword.get(:scheme)
+
+ if scheme == "https" do
+ put_resp_header(
+ conn,
+ "strict-transport-security",
+ "max-age=63072000; includeSubDomains; preload"
+ )
+ else
+ conn
+ end
+ end
+
def real_ip_opts do
[
headers: ["x-forwarded-for"],
diff --git a/elixir/apps/web/lib/web/live/actors/service_accounts/new.ex b/elixir/apps/web/lib/web/live/actors/service_accounts/new.ex
index 3f7806c17..f3554b050 100644
--- a/elixir/apps/web/lib/web/live/actors/service_accounts/new.ex
+++ b/elixir/apps/web/lib/web/live/actors/service_accounts/new.ex
@@ -84,6 +84,22 @@ defmodule Web.Actors.ServiceAccounts.New do
{:noreply, socket}
else
+ {:error, :service_accounts_limit_reached} ->
+ changeset =
+ attrs
+ |> Actors.new_actor()
+ |> Map.put(:action, :insert)
+
+ socket =
+ socket
+ |> put_flash(
+ :error,
+ "You have reached the maximum number of service accounts allowed by your subscription plan."
+ )
+ |> assign(form: to_form(changeset))
+
+ {:noreply, socket}
+
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
diff --git a/elixir/apps/web/lib/web/live/actors/show.ex b/elixir/apps/web/lib/web/live/actors/show.ex
index 738ccd2f8..45d7b97b5 100644
--- a/elixir/apps/web/lib/web/live/actors/show.ex
+++ b/elixir/apps/web/lib/web/live/actors/show.ex
@@ -1,7 +1,7 @@
defmodule Web.Actors.Show do
use Web, :live_view
import Web.Actors.Components
- alias Domain.{Auth, Tokens, Flows, Clients}
+ alias Domain.{Accounts, Auth, Tokens, Flows, Clients}
alias Domain.Actors
def mount(%{"id" => id}, _token, socket) do
@@ -30,7 +30,7 @@ defmodule Web.Actors.Show do
flows: flows,
tokens: tokens,
page_title: "Actor #{actor.name}",
- flow_activities_enabled?: Domain.Config.flow_activities_enabled?()
+ flow_activities_enabled?: Accounts.flow_activities_enabled?(socket.assigns.account)
)}
else
_other -> raise Web.LiveErrors.NotFoundError
diff --git a/elixir/apps/web/lib/web/live/actors/users/new.ex b/elixir/apps/web/lib/web/live/actors/users/new.ex
index 4a2dbccf3..a69d58d22 100644
--- a/elixir/apps/web/lib/web/live/actors/users/new.ex
+++ b/elixir/apps/web/lib/web/live/actors/users/new.ex
@@ -76,6 +76,22 @@ defmodule Web.Actors.Users.New do
{:noreply, socket}
else
+ {:error, :seats_limit_reached} ->
+ changeset =
+ attrs
+ |> Actors.new_actor()
+ |> Map.put(:action, :insert)
+
+ socket =
+ socket
+ |> put_flash(
+ :error,
+ "You have reached the maximum number of seats allowed by your subscription plan."
+ )
+ |> assign(form: to_form(changeset))
+
+ {:noreply, socket}
+
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
diff --git a/elixir/apps/web/lib/web/live/clients/show.ex b/elixir/apps/web/lib/web/live/clients/show.ex
index ead1c3a54..2c6bd06e3 100644
--- a/elixir/apps/web/lib/web/live/clients/show.ex
+++ b/elixir/apps/web/lib/web/live/clients/show.ex
@@ -1,7 +1,7 @@
defmodule Web.Clients.Show do
use Web, :live_view
import Web.Policies.Components
- alias Domain.{Clients, Flows, Config}
+ alias Domain.{Accounts, Clients, Flows}
def mount(%{"id" => id}, _session, socket) do
with {:ok, client} <-
@@ -19,7 +19,7 @@ defmodule Web.Clients.Show do
socket,
client: client,
flows: flows,
- flow_activities_enabled?: Config.flow_activities_enabled?(),
+ flow_activities_enabled?: Accounts.flow_activities_enabled?(socket.assigns.account),
page_title: "Client #{client.name}"
)
diff --git a/elixir/apps/web/lib/web/live/policies/show.ex b/elixir/apps/web/lib/web/live/policies/show.ex
index 373fc9eb5..d8da87b72 100644
--- a/elixir/apps/web/lib/web/live/policies/show.ex
+++ b/elixir/apps/web/lib/web/live/policies/show.ex
@@ -1,7 +1,7 @@
defmodule Web.Policies.Show do
use Web, :live_view
import Web.Policies.Components
- alias Domain.{Policies, Flows, Config}
+ alias Domain.{Accounts, Policies, Flows}
def mount(%{"id" => id}, _session, socket) do
with {:ok, policy} <-
@@ -19,7 +19,7 @@ defmodule Web.Policies.Show do
policy: policy,
flows: flows,
page_title: "Policy #{policy.id}",
- flow_activities_enabled?: Config.flow_activities_enabled?()
+ flow_activities_enabled?: Accounts.flow_activities_enabled?(socket.assigns.account)
)
{:ok, socket}
diff --git a/elixir/apps/web/lib/web/live/relay_groups/edit.ex b/elixir/apps/web/lib/web/live/relay_groups/edit.ex
index ea571a3d0..e1206fd5c 100644
--- a/elixir/apps/web/lib/web/live/relay_groups/edit.ex
+++ b/elixir/apps/web/lib/web/live/relay_groups/edit.ex
@@ -1,9 +1,9 @@
defmodule Web.RelayGroups.Edit do
use Web, :live_view
- alias Domain.Relays
+ alias Domain.{Accounts, Relays}
def mount(%{"id" => id}, _session, socket) do
- with true <- Domain.Config.self_hosted_relays_enabled?(),
+ with true <- Accounts.self_hosted_relays_enabled?(socket.assigns.account),
{:ok, group} <- Relays.fetch_group_by_id(id, socket.assigns.subject),
nil <- group.deleted_at do
changeset = Relays.change_group(group)
diff --git a/elixir/apps/web/lib/web/live/relay_groups/index.ex b/elixir/apps/web/lib/web/live/relay_groups/index.ex
index 9ecd0bfec..045d01f12 100644
--- a/elixir/apps/web/lib/web/live/relay_groups/index.ex
+++ b/elixir/apps/web/lib/web/live/relay_groups/index.ex
@@ -1,11 +1,11 @@
defmodule Web.RelayGroups.Index do
use Web, :live_view
- alias Domain.Relays
+ alias Domain.{Accounts, Relays}
def mount(_params, _session, socket) do
subject = socket.assigns.subject
- with true <- Domain.Config.self_hosted_relays_enabled?(),
+ with true <- Accounts.self_hosted_relays_enabled?(socket.assigns.account),
{:ok, groups} <- Relays.list_groups(subject, preload: [:relays]) do
:ok = Relays.subscribe_to_relays_presence_in_account(socket.assigns.account)
diff --git a/elixir/apps/web/lib/web/live/relay_groups/new.ex b/elixir/apps/web/lib/web/live/relay_groups/new.ex
index 1c4486908..4392866b2 100644
--- a/elixir/apps/web/lib/web/live/relay_groups/new.ex
+++ b/elixir/apps/web/lib/web/live/relay_groups/new.ex
@@ -1,9 +1,9 @@
defmodule Web.RelayGroups.New do
use Web, :live_view
- alias Domain.Relays
+ alias Domain.{Accounts, Relays}
def mount(_params, _session, socket) do
- with true <- Domain.Config.self_hosted_relays_enabled?() do
+ with true <- Accounts.self_hosted_relays_enabled?(socket.assigns.account) do
changeset = Relays.new_group()
{:ok, assign(socket, form: to_form(changeset, page_title: "New Relay Group")),
diff --git a/elixir/apps/web/lib/web/live/relay_groups/new_token.ex b/elixir/apps/web/lib/web/live/relay_groups/new_token.ex
index 7a61ef0b0..0398298f0 100644
--- a/elixir/apps/web/lib/web/live/relay_groups/new_token.ex
+++ b/elixir/apps/web/lib/web/live/relay_groups/new_token.ex
@@ -1,9 +1,9 @@
defmodule Web.RelayGroups.NewToken do
use Web, :live_view
- alias Domain.Relays
+ alias Domain.{Accounts, Relays}
def mount(%{"id" => id}, _session, socket) do
- with true <- Domain.Config.self_hosted_relays_enabled?(),
+ with true <- Accounts.self_hosted_relays_enabled?(socket.assigns.account),
{:ok, group} <- Relays.fetch_group_by_id(id, socket.assigns.subject) do
{group, env} =
if connected?(socket) do
diff --git a/elixir/apps/web/lib/web/live/relay_groups/show.ex b/elixir/apps/web/lib/web/live/relay_groups/show.ex
index e4fe28d9e..812b61542 100644
--- a/elixir/apps/web/lib/web/live/relay_groups/show.ex
+++ b/elixir/apps/web/lib/web/live/relay_groups/show.ex
@@ -1,9 +1,9 @@
defmodule Web.RelayGroups.Show do
use Web, :live_view
- alias Domain.{Relays, Tokens}
+ alias Domain.{Accounts, Relays, Tokens}
def mount(%{"id" => id}, _session, socket) do
- with true <- Domain.Config.self_hosted_relays_enabled?(),
+ with true <- Accounts.self_hosted_relays_enabled?(socket.assigns.account),
{:ok, group} <-
Relays.fetch_group_by_id(id, socket.assigns.subject,
preload: [
diff --git a/elixir/apps/web/lib/web/live/relays/show.ex b/elixir/apps/web/lib/web/live/relays/show.ex
index b4eee1929..7edbdfb82 100644
--- a/elixir/apps/web/lib/web/live/relays/show.ex
+++ b/elixir/apps/web/lib/web/live/relays/show.ex
@@ -1,9 +1,9 @@
defmodule Web.Relays.Show do
use Web, :live_view
- alias Domain.{Relays, Config}
+ alias Domain.{Accounts, Relays}
def mount(%{"id" => id}, _session, socket) do
- with true <- Config.self_hosted_relays_enabled?(),
+ with true <- Accounts.self_hosted_relays_enabled?(socket.assigns.account),
{:ok, relay} <-
Relays.fetch_relay_by_id(id, socket.assigns.subject, preload: :group) do
:ok = Relays.subscribe_to_relays_presence_in_group(relay.group)
diff --git a/elixir/apps/web/lib/web/live/resources/components.ex b/elixir/apps/web/lib/web/live/resources/components.ex
index b37ae1257..2a65c259e 100644
--- a/elixir/apps/web/lib/web/live/resources/components.ex
+++ b/elixir/apps/web/lib/web/live/resources/components.ex
@@ -4,9 +4,9 @@ defmodule Web.Resources.Components do
defp pretty_print_ports([]), do: ""
defp pretty_print_ports(ports), do: Enum.join(ports, ", ")
- def map_filters_form_attrs(attrs) do
+ def map_filters_form_attrs(attrs, account) do
attrs =
- if Domain.Config.traffic_filters_enabled?() do
+ if Domain.Accounts.traffic_filters_enabled?(account) do
attrs
else
Map.put(attrs, "filters", %{"all" => %{"enabled" => "true", "protocol" => "all"}})
diff --git a/elixir/apps/web/lib/web/live/resources/edit.ex b/elixir/apps/web/lib/web/live/resources/edit.ex
index fb9289b08..ef6d161ed 100644
--- a/elixir/apps/web/lib/web/live/resources/edit.ex
+++ b/elixir/apps/web/lib/web/live/resources/edit.ex
@@ -1,7 +1,7 @@
defmodule Web.Resources.Edit do
use Web, :live_view
import Web.Resources.Components
- alias Domain.{Gateways, Resources, Config}
+ alias Domain.{Accounts, Gateways, Resources}
def mount(%{"id" => id} = params, _session, socket) do
with {:ok, resource} <-
@@ -17,7 +17,7 @@ defmodule Web.Resources.Edit do
gateway_groups: gateway_groups,
form: form,
params: Map.take(params, ["site_id"]),
- traffic_filters_enabled?: Config.traffic_filters_enabled?(),
+ traffic_filters_enabled?: Accounts.traffic_filters_enabled?(socket.assigns.account),
page_title: "Edit #{resource.name}"
)
@@ -92,7 +92,7 @@ defmodule Web.Resources.Edit do
def handle_event("change", %{"resource" => attrs}, socket) do
attrs =
attrs
- |> map_filters_form_attrs()
+ |> map_filters_form_attrs(socket.assigns.account)
|> map_connections_form_attrs()
|> maybe_delete_connections(socket.assigns.params)
@@ -106,7 +106,7 @@ defmodule Web.Resources.Edit do
def handle_event("submit", %{"resource" => attrs}, socket) do
attrs =
attrs
- |> map_filters_form_attrs()
+ |> map_filters_form_attrs(socket.assigns.account)
|> map_connections_form_attrs()
|> maybe_delete_connections(socket.assigns.params)
diff --git a/elixir/apps/web/lib/web/live/resources/index.ex b/elixir/apps/web/lib/web/live/resources/index.ex
index 78a670ce3..8ed0ac0ea 100644
--- a/elixir/apps/web/lib/web/live/resources/index.ex
+++ b/elixir/apps/web/lib/web/live/resources/index.ex
@@ -44,7 +44,7 @@ defmodule Web.Resources.Index do
<:action>
<.add_button
- :if={Domain.Config.multi_site_resources_enabled?()}
+ :if={Domain.Accounts.multi_site_resources_enabled?(@account)}
navigate={~p"/#{@account}/resources/new"}
>
Add Multi-Site Resource
diff --git a/elixir/apps/web/lib/web/live/resources/new.ex b/elixir/apps/web/lib/web/live/resources/new.ex
index 8b52601da..b4b32fcd4 100644
--- a/elixir/apps/web/lib/web/live/resources/new.ex
+++ b/elixir/apps/web/lib/web/live/resources/new.ex
@@ -1,7 +1,7 @@
defmodule Web.Resources.New do
use Web, :live_view
import Web.Resources.Components
- alias Domain.{Gateways, Resources, Config}
+ alias Domain.{Accounts, Gateways, Resources}
def mount(params, _session, socket) do
with {:ok, gateway_groups} <- Gateways.list_groups(socket.assigns.subject) do
@@ -15,7 +15,7 @@ defmodule Web.Resources.New do
name_changed?: false,
form: to_form(changeset),
params: Map.take(params, ["site_id"]),
- traffic_filters_enabled?: Config.traffic_filters_enabled?(),
+ traffic_filters_enabled?: Accounts.traffic_filters_enabled?(socket.assigns.account),
page_title: "New Resource"
)
@@ -165,7 +165,7 @@ defmodule Web.Resources.New do
attrs
|> maybe_put_default_name(name_changed?)
|> maybe_put_default_address_description(address_description_changed?)
- |> map_filters_form_attrs()
+ |> map_filters_form_attrs(socket.assigns.account)
|> map_connections_form_attrs()
|> maybe_put_connections(socket.assigns.params)
@@ -188,7 +188,7 @@ defmodule Web.Resources.New do
attrs
|> maybe_put_default_name()
|> maybe_put_default_address_description()
- |> map_filters_form_attrs()
+ |> map_filters_form_attrs(socket.assigns.account)
|> map_connections_form_attrs()
|> maybe_put_connections(socket.assigns.params)
diff --git a/elixir/apps/web/lib/web/live/resources/show.ex b/elixir/apps/web/lib/web/live/resources/show.ex
index 1b8154d15..b5dc8542a 100644
--- a/elixir/apps/web/lib/web/live/resources/show.ex
+++ b/elixir/apps/web/lib/web/live/resources/show.ex
@@ -1,7 +1,7 @@
defmodule Web.Resources.Show do
use Web, :live_view
import Web.Policies.Components
- alias Domain.{Resources, Flows, Config}
+ alias Domain.{Accounts, Resources, Flows}
def mount(%{"id" => id} = params, _session, socket) do
with {:ok, resource} <-
@@ -23,7 +23,7 @@ defmodule Web.Resources.Show do
actor_groups_peek: Map.fetch!(actor_groups_peek, resource.id),
flows: flows,
params: Map.take(params, ["site_id"]),
- traffic_filters_enabled?: Config.traffic_filters_enabled?(),
+ traffic_filters_enabled?: Accounts.traffic_filters_enabled?(socket.assigns.account),
page_title: "Resource #{resource.name}"
)
@@ -48,7 +48,7 @@ defmodule Web.Resources.Show do
<:action :if={is_nil(@resource.deleted_at)}>
<.edit_button
- :if={Domain.Config.multi_site_resources_enabled?()}
+ :if={Domain.Accounts.multi_site_resources_enabled?(@account)}
navigate={~p"/#{@account}/resources/#{@resource.id}/edit?#{@params}"}
>
Edit Resource
diff --git a/elixir/apps/web/lib/web/live/settings/account.ex b/elixir/apps/web/lib/web/live/settings/account.ex
index 41470c49a..098c8d4b5 100644
--- a/elixir/apps/web/lib/web/live/settings/account.ex
+++ b/elixir/apps/web/lib/web/live/settings/account.ex
@@ -1,8 +1,14 @@
defmodule Web.Settings.Account do
use Web, :live_view
+ alias Domain.Accounts
def mount(_params, _session, socket) do
- {:ok, assign(socket, page_title: "Account")}
+ socket =
+ assign(socket,
+ page_title: "Account"
+ )
+
+ {:ok, socket}
end
def render(assigns) do
@@ -16,26 +22,25 @@ defmodule Web.Settings.Account do
Account Settings
<:content>
-
- <.vertical_table id="account">
- <.vertical_table_row>
- <:label>Account Name
- <:value><%= @account.name %>
-
- <.vertical_table_row>
- <:label>Account ID
- <:value><%= @account.id %>
-
- <.vertical_table_row>
- <:label>Account Slug
- <:value>
- <.copy id="account-slug"><%= @account.slug %>
-
-
-
-
+ <.vertical_table id="account">
+ <.vertical_table_row>
+ <:label>Account Name
+ <:value><%= @account.name %>
+
+ <.vertical_table_row>
+ <:label>Account ID
+ <:value><%= @account.id %>
+
+ <.vertical_table_row>
+ <:label>Account Slug
+ <:value>
+ <.copy id="account-slug"><%= @account.slug %>
+
+
+
+
<.section>
<:title>
Danger zone
@@ -45,10 +50,11 @@ defmodule Web.Settings.Account do
Terminate account
- <.icon name="hero-exclamation-circle" class="inline-block w-5 h-5 mr-1 text-red-500" />
- To disable your account and schedule it for deletion, please <.link
+ <.icon name="hero-exclamation-circle" class="inline-block w-5 h-5 mr-1 text-red-500" /> To
+ disable your account and
+ schedule it for deletion, please <.link
class={link_style()}
- href="mailto:support@firezone.dev"
+ href={mailto_support(@account, @subject, "Account termination request: #{@account.name}")}
>contact support.
diff --git a/elixir/apps/web/lib/web/live/settings/billing.ex b/elixir/apps/web/lib/web/live/settings/billing.ex
new file mode 100644
index 000000000..1d082cbcc
--- /dev/null
+++ b/elixir/apps/web/lib/web/live/settings/billing.ex
@@ -0,0 +1,215 @@
+defmodule Web.Settings.Billing do
+ use Web, :live_view
+ alias Domain.{Accounts, Actors, Clients, Gateways, Billing}
+ require Logger
+
+ def mount(_params, _session, socket) do
+ unless Billing.account_provisioned?(socket.assigns.account),
+ do: raise(Web.LiveErrors.NotFoundError)
+
+ admins_count = Actors.count_account_admin_users_for_account(socket.assigns.account)
+ service_accounts_count = Actors.count_service_accounts_for_account(socket.assigns.account)
+ active_users_count = Clients.count_1m_active_users_for_account(socket.assigns.account)
+ gateway_groups_count = Gateways.count_groups_for_account(socket.assigns.account)
+
+ socket =
+ assign(socket,
+ error: nil,
+ page_title: "Billing",
+ admins_count: admins_count,
+ active_users_count: active_users_count,
+ service_accounts_count: service_accounts_count,
+ gateway_groups_count: gateway_groups_count
+ )
+
+ {:ok, socket}
+ end
+
+ def render(assigns) do
+ ~H"""
+ <.breadcrumbs account={@account}>
+ <.breadcrumb path={~p"/#{@account}/settings/billing"}>Billing
+
+
+ <.section>
+ <:title>
+ Billing Information
+
+ <:action>
+ <.button icon="hero-pencil" phx-click="redirect_to_billing_portal">
+ Manage
+
+ <.button navigate={
+ mailto_support(
+ @account,
+ @subject,
+ "Billing question: #{@account.name}"
+ )
+ }>
+ Contact Sales Team
+
+
+ <:content>
+ <.flash :if={@error} kind={:error}>
+ <%= @error %>
+
+
+ If you need assistance, please <.link
+ class={link_style()}
+ href={
+ mailto_support(
+ @account,
+ @subject,
+ "Issues accessing billing portal: #{@account.name}"
+ )
+ }
+ >contact support.
+
+
+
+ <.vertical_table id="billing">
+ <.vertical_table_row>
+ <:label>Current Plan
+ <:value>
+ <%= @account.metadata.stripe.product_name %>
+
+
+
+ <.vertical_table_row :if={not is_nil(@account.limits.monthly_active_users_count)}>
+ <:label>
+ Seats
+
+ <:value>
+ @account.limits.monthly_active_users_count && "text-red-500"
+ ]}>
+ <%= @active_users_count %> used
+
+ / <%= @account.limits.monthly_active_users_count %> allowed
+ users with at least one device signed-in within last month
+
+
+
+ <.vertical_table_row :if={not is_nil(@account.limits.service_accounts_count)}>
+ <:label>
+ Service Accounts
+
+ <:value>
+ @account.limits.service_accounts_count && "text-red-500"
+ ]}>
+ <%= @service_accounts_count %> used
+
+ / <%= @account.limits.service_accounts_count %> allowed
+ users with at least one device signed-in within last month
+
+
+
+ <.vertical_table_row :if={not is_nil(@account.limits.account_admin_users_count)}>
+ <:label>
+ Admins
+
+ <:value>
+ @account.limits.account_admin_users_count && "text-red-500"
+ ]}>
+ <%= @admins_count %> used
+
+ / <%= @account.limits.account_admin_users_count %> allowed
+
+
+
+ <.vertical_table_row :if={not is_nil(@account.limits.gateway_groups_count)}>
+ <:label>
+ Sites
+
+ <:value>
+ @account.limits.gateway_groups_count && "text-red-500"
+ ]}>
+ <%= @gateway_groups_count %> used
+
+ / <%= @account.limits.gateway_groups_count %> allowed
+
+
+
+
+
+
+ <.section>
+ <:title>
+ Enabled Enterprise Features
+
+ <:help>
+ For further details on enrolling in beta features, reach out to your account manager
+
+ <:content>
+ <.vertical_table id="features">
+ <.vertical_table_row :for={
+ {key, _value} <- Map.delete(Map.from_struct(@account.features), :limits)
+ }>
+ <:label><.feature_name feature={key} />
+ <:value>
+ <% value = apply(Domain.Accounts, :"#{key}_enabled?", [@account]) %>
+ <.icon
+ :if={value == true}
+ name="hero-check"
+ class="inline-block w-5 h-5 mr-1 text-green-500"
+ />
+ <.icon
+ :if={value == false}
+ name="hero-x-mark"
+ class="inline-block w-5 h-5 mr-1 text-red-500"
+ />
+
+
+
+
+
+
+ <.section>
+ <:title>
+ Danger zone
+
+ <:content>
+
+ Terminate account
+
+
+ <.icon name="hero-exclamation-circle" class="inline-block w-5 h-5 mr-1 text-red-500" /> To
+ disable your account and
+ schedule it for deletion, please <.link
+ class={link_style()}
+ href={mailto_support(@account, @subject, "Account termination request: #{@account.name}")}
+ >contact support.
+
+
+
+ """
+ end
+
+ def handle_event("redirect_to_billing_portal", _params, socket) do
+ with {:ok, billing_portal_url} <-
+ Billing.billing_portal_url(
+ socket.assigns.account,
+ url(~p"/#{socket.assigns.account}/settings/billing"),
+ socket.assigns.subject
+ ) do
+ {:noreply, redirect(socket, external: billing_portal_url)}
+ else
+ {:error, reason} ->
+ Logger.error("Failed to get billing portal URL", reason: inspect(reason))
+
+ socket =
+ assign(socket,
+ error: "Billing portal is temporarily unavailable, please try again later."
+ )
+
+ {:noreply, socket}
+ end
+ end
+end
diff --git a/elixir/apps/web/lib/web/live/settings/dns.ex b/elixir/apps/web/lib/web/live/settings/dns.ex
index 059bd4923..9794cf66c 100644
--- a/elixir/apps/web/lib/web/live/settings/dns.ex
+++ b/elixir/apps/web/lib/web/live/settings/dns.ex
@@ -1,25 +1,26 @@
defmodule Web.Settings.DNS do
use Web, :live_view
- alias Domain.Config
- alias Domain.Config.Configuration.ClientsUpstreamDNS
+ alias Domain.Accounts
def mount(_params, _session, socket) do
- {:ok, config} = Config.fetch_account_config(socket.assigns.subject)
+ account = Accounts.fetch_account_by_id!(socket.assigns.account.id)
form =
- Config.change_account_config(config, %{})
- |> add_new_server()
+ Accounts.change_account(account, %{})
+ |> maybe_append_empty_embed()
|> to_form()
- socket = assign(socket, config: config, form: form, page_title: "DNS")
+ socket =
+ assign(socket,
+ account: account,
+ form: form,
+ page_title: "DNS"
+ )
{:ok, socket}
end
def render(assigns) do
- assigns =
- assign(assigns, :errors, translate_errors(assigns.form.errors, :clients_upstream_dns))
-
~H"""
<.breadcrumbs account={@account}>
<.breadcrumb path={~p"/#{@account}/settings/dns"}>DNS Settings
@@ -56,26 +57,37 @@ defmodule Web.Settings.DNS do
<.form for={@form} phx-submit={:submit} phx-change={:change}>
- <.inputs_for :let={dns} field={@form[:clients_upstream_dns]}>
-
-
- <.input
- type="select"
- label="Protocol"
- field={dns[:protocol]}
- placeholder="Protocol"
- options={dns_options()}
- value={dns[:protocol].value}
- />
+ <.inputs_for :let={config} field={@form[:config]}>
+ <.inputs_for :let={dns} field={config[:clients_upstream_dns]}>
+
+
+ <.input
+ type="select"
+ label="Protocol"
+ field={dns[:protocol]}
+ placeholder="Protocol"
+ options={dns_options()}
+ value={dns[:protocol].value}
+ />
+
+
+ <.input
+ label="Address"
+ field={dns[:address]}
+ placeholder="DNS Server Address"
+ />
+
-
- <.input label="Address" field={dns[:address]} placeholder="DNS Server Address" />
-
-
+
+ <% errors =
+ translate_errors(
+ @form.source.changes.config.errors,
+ :clients_upstream_dns
+ ) %>
+ <.error :for={error <- errors} data-validation-error-for="clients_upstream_dns">
+ <%= error %>
+
- <.error :for={msg <- @errors} data-validation-error-for="clients_upstream_dns">
- <%= msg %>
-
<.submit_button>
Save
@@ -88,87 +100,102 @@ defmodule Web.Settings.DNS do
"""
end
- def handle_event("change", %{"configuration" => config_params}, socket) do
- form =
- Config.change_account_config(socket.assigns.config, config_params)
+ def handle_event("change", %{"account" => attrs}, socket) do
+ changeset =
+ Accounts.change_account(socket.assigns.account, attrs)
+ |> maybe_append_empty_embed()
|> filter_errors()
|> Map.put(:action, :validate)
- |> to_form()
- socket = assign(socket, form: form)
- {:noreply, socket}
+ {:noreply, assign(socket, form: to_form(changeset))}
end
- def handle_event("submit", %{"configuration" => config_params}, socket) do
- attrs = remove_empty_servers(config_params)
+ def handle_event("submit", %{"account" => attrs}, socket) do
+ attrs = remove_empty_servers(attrs)
- with {:ok, new_config} <-
- Domain.Config.update_config(socket.assigns.config, attrs, socket.assigns.subject) do
+ with {:ok, account} <-
+ Accounts.update_account(socket.assigns.account, attrs, socket.assigns.subject) do
form =
- Config.change_account_config(new_config, %{})
- |> add_new_server()
+ Accounts.change_account(account, %{})
+ |> maybe_append_empty_embed()
|> to_form()
- socket = assign(socket, config: new_config, form: form)
- {:noreply, socket}
+ {:noreply, assign(socket, account: account, form: form)}
else
{:error, changeset} ->
- form = to_form(changeset)
- socket = assign(socket, form: form)
- {:noreply, socket}
+ changeset =
+ changeset
+ |> maybe_append_empty_embed()
+ |> filter_errors()
+ |> Map.put(:action, :validate)
+
+ {:noreply, assign(socket, form: to_form(changeset))}
end
end
- defp remove_errors(changeset, field, message) do
- errors =
- Enum.filter(changeset.errors, fn
- {^field, {^message, _}} -> false
- {_, _} -> true
- end)
-
- %{changeset | errors: errors}
- end
-
- defp filter_errors(%{changes: %{clients_upstream_dns: clients_upstream_dns}} = changeset) do
- filtered_cs =
- changeset
- |> remove_errors(:clients_upstream_dns, "address can't be blank")
-
- filtered_dns_cs =
- clients_upstream_dns
- |> Enum.map(fn changeset ->
- remove_errors(changeset, :address, "can't be blank")
- end)
-
- %{filtered_cs | changes: %{clients_upstream_dns: filtered_dns_cs}}
- end
-
defp filter_errors(changeset) do
- changeset
+ 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_empty_servers(config) do
- servers =
- config["clients_upstream_dns"]
- |> Enum.reduce(%{}, fn {key, value}, acc ->
- case value["address"] do
- nil -> acc
- "" -> acc
- _ -> Map.put(acc, key, value)
+ 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)
-
- %{"clients_upstream_dns" => servers}
+ end)
end
- defp add_new_server(changeset) do
- existing_servers = Ecto.Changeset.get_embed(changeset, :clients_upstream_dns)
+ defp update_clients_upstream_dns(changeset, cb) do
+ config_changeset = Ecto.Changeset.get_embed(changeset, :config)
- Ecto.Changeset.put_embed(
- changeset,
- :clients_upstream_dns,
- existing_servers ++ [%{address: ""}]
- )
+ 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
@@ -178,10 +205,10 @@ defmodule Web.Settings.DNS do
[key: "DNS over HTTPS", value: "dns_over_https"]
]
- supported = Enum.map(ClientsUpstreamDNS.supported_protocols(), &to_string/1)
+ supported_dns_protocols = Enum.map(Accounts.Config.supported_dns_protocols(), &to_string/1)
Enum.map(options, fn option ->
- case option[:value] in supported do
+ case option[:value] in supported_dns_protocols do
true -> option
false -> option ++ [disabled: true]
end
diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/new.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/new.ex
index b76f17ad0..50756aa1b 100644
--- a/elixir/apps/web/lib/web/live/settings/identity_providers/new.ex
+++ b/elixir/apps/web/lib/web/live/settings/identity_providers/new.ex
@@ -3,8 +3,15 @@ defmodule Web.Settings.IdentityProviders.New do
alias Domain.Auth
def mount(_params, _session, socket) do
- {:ok, adapters} = Auth.list_provider_adapters()
- socket = assign(socket, form: %{}, adapters: adapters, page_title: "New Identity Provider")
+ adapters = Auth.list_user_provisioned_provider_adapters!(socket.assigns.account)
+
+ socket =
+ assign(socket,
+ form: %{},
+ adapters: adapters,
+ page_title: "New Identity Provider"
+ )
+
{:ok, socket}
end
@@ -34,7 +41,12 @@ defmodule Web.Settings.IdentityProviders.New do
<.submit_button>
@@ -52,7 +64,7 @@ defmodule Web.Settings.IdentityProviders.New do
<.adapter_item
adapter={@adapter}
account={@account}
- enterprise_feature={true}
+ opts={@opts}
name="Google Workspace"
description="Authenticate users and synchronize users and groups with a custom Google Workspace connector."
/>
@@ -64,7 +76,7 @@ defmodule Web.Settings.IdentityProviders.New do
<.adapter_item
adapter={@adapter}
account={@account}
- enterprise_feature={true}
+ opts={@opts}
name="Microsoft Entra"
description="Authenticate users and synchronize users and groups with a custom Microsoft Entra connector."
/>
@@ -76,7 +88,7 @@ defmodule Web.Settings.IdentityProviders.New do
<.adapter_item
adapter={@adapter}
account={@account}
- enterprise_feature={true}
+ opts={@opts}
name="Okta"
description="Authenticate users and synchronize users and groups with a custom Okta connector."
/>
@@ -88,26 +100,16 @@ defmodule Web.Settings.IdentityProviders.New do
<.adapter_item
adapter={@adapter}
account={@account}
+ opts={@opts}
name="OpenID Connect"
description="Authenticate users with a universal OpenID Connect adapter and manager users and groups manually."
/>
"""
end
- def adapter(%{adapter: :saml} = assigns) do
- ~H"""
- <.adapter_item
- adapter={@adapter}
- account={@account}
- name="SAML 2.0"
- description="Authenticate users with a custom SAML 2.0 adapter and synchronize users and groups with SCIM 2.0."
- />
- """
- end
-
attr :adapter, :any
attr :account, :any
- attr :enterprise_feature, :boolean, default: false
+ attr :opts, :any
attr :name, :string
attr :description, :string
@@ -120,17 +122,24 @@ defmodule Web.Settings.IdentityProviders.New do
type="radio"
name="next"
value={next_step_path(@adapter, @account)}
- class={~w[ w-4 h-4 border-neutral-300 ]}
+ class={[
+ "w-4 h-4 border-neutral-300",
+ @opts[:enabled] == false && "cursor-not-allowed"
+ ]}
+ disabled={@opts[:enabled] == false}
required
/>
<.provider_icon adapter={@adapter} class="w-8 h-8 ml-4" />
- <%= if @enterprise_feature do %>
- <.badge class="ml-2" type="primary" title="Feature available on the Enterprise plan">
- ENTERPRISE
-
+
+ <%= if @opts[:enabled] == false do %>
+ <.link navigate={~p"/#{@account}/settings/billing"} class="ml-2 text-sm text-primary-500">
+ <.badge class="ml-2" type="primary" title="Feature available on a higher pricing plan">
+ UPGRADE TO UNLOCK
+
+
<% end %>
diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/system/show.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/system/show.ex
index d80d851a4..086ec7b1c 100644
--- a/elixir/apps/web/lib/web/live/settings/identity_providers/system/show.ex
+++ b/elixir/apps/web/lib/web/live/settings/identity_providers/system/show.ex
@@ -86,17 +86,6 @@ defmodule Web.Settings.IdentityProviders.System.Show do
-
- <.danger_zone :if={is_nil(@provider.deleted_at)}>
- <:action>
- <.delete_button
- data-confirm="Are you sure want to delete this provider along with all related data?"
- phx-click="delete"
- >
- Delete Identity Provider
-
-
-
"""
end
diff --git a/elixir/apps/web/lib/web/live/sign_in.ex b/elixir/apps/web/lib/web/live/sign_in.ex
index b6c07f2b3..2c08f910c 100644
--- a/elixir/apps/web/lib/web/live/sign_in.ex
+++ b/elixir/apps/web/lib/web/live/sign_in.ex
@@ -58,7 +58,11 @@ defmodule Web.SignIn do
<.flash flash={@flash} kind={:error} />
<.flash flash={@flash} kind={:info} />
- <.intersperse_blocks>
+ <.flash :if={not Accounts.account_active?(@account)} kind={:error} style="wide">
+ This account has been disabled, please contact your administrator.
+
+
+ <.intersperse_blocks :if={not disabled?(@account, @params)}>
<:separator>
<.separator />
@@ -115,6 +119,14 @@ defmodule Web.SignIn do
"""
end
+ def disabled?(account, params) do
+ # We allow to sign in to Web UI even for disabled accounts
+ case Web.Auth.fetch_auth_context_type!(params) do
+ :client -> not Accounts.account_active?(account)
+ :browser -> false
+ end
+ end
+
def separator(assigns) do
~H"""
diff --git a/elixir/apps/web/lib/web/live/sign_up.ex b/elixir/apps/web/lib/web/live/sign_up.ex
index 43a5de23f..08e8cf6a4 100644
--- a/elixir/apps/web/lib/web/live/sign_up.ex
+++ b/elixir/apps/web/lib/web/live/sign_up.ex
@@ -149,7 +149,7 @@ defmodule Web.SignUp do
-
+
<.form
for={%{}}
id="resend-email"
@@ -163,9 +163,10 @@ defmodule Web.SignUp do
name="email[provider_identifier]"
value={@identity.provider_identifier}
/>
- <.submit_button>
+
+ <.button type="submit" class="w-full">
Sign In
-
+
@@ -333,6 +334,8 @@ defmodule Web.SignUp do
case Domain.Repo.transaction(multi) do
{:ok, %{account: account, provider: provider, identity: identity}} ->
+ {:ok, account} = Domain.Billing.provision_account(account)
+
{:ok, _} =
Web.Mailer.AuthEmail.sign_up_link_email(
account,
diff --git a/elixir/apps/web/lib/web/live/sites/new.ex b/elixir/apps/web/lib/web/live/sites/new.ex
index b8c7da44f..480cfbe8f 100644
--- a/elixir/apps/web/lib/web/live/sites/new.ex
+++ b/elixir/apps/web/lib/web/live/sites/new.ex
@@ -21,6 +21,7 @@ defmodule Web.Sites.New do
Add a new Site
<:content>
+ <.flash kind={:error} flash={@flash} />
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<.input type="hidden" field={@form[:routing]} value="managed" />
@@ -52,6 +53,21 @@ defmodule Web.Sites.New do
Gateways.create_group(attrs, socket.assigns.subject) do
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/sites/#{group}")}
else
+ {:error, :gateway_groups_limit_reached} ->
+ changeset =
+ Gateways.new_group(attrs)
+ |> Map.put(:action, :insert)
+
+ socket =
+ socket
+ |> put_flash(
+ :error,
+ "You have reached the maximum number of sites allowed by your subscription plan."
+ )
+ |> assign(form: to_form(changeset))
+
+ {:noreply, socket}
+
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
diff --git a/elixir/apps/web/lib/web/router.ex b/elixir/apps/web/lib/web/router.ex
index a50461609..44555a132 100644
--- a/elixir/apps/web/lib/web/router.ex
+++ b/elixir/apps/web/lib/web/router.ex
@@ -10,12 +10,6 @@ defmodule Web.Router do
plug :put_root_layout, {Web.Layouts, :root}
end
- pipeline :api do
- plug :accepts, ["json"]
- plug :ensure_authenticated
- plug :ensure_authenticated_actor_type, :service_account
- end
-
pipeline :public do
plug :accepts, ["html", "xml"]
end
@@ -136,8 +130,6 @@ defmodule Web.Router do
end
live "/:id/edit", Edit
- # TODO: REMOVEME it's just another identity
- live "/:id/new_token", NewToken
end
scope "/groups", Groups do
@@ -204,6 +196,7 @@ defmodule Web.Router do
scope "/settings", Settings do
live "/account", Account
+ live "/billing", Billing
scope "/identity_providers", IdentityProviders do
live "/", Index
diff --git a/elixir/apps/web/priv/static/.well-known/apple-developer-merchantid-domain-association b/elixir/apps/web/priv/static/.well-known/apple-developer-merchantid-domain-association
new file mode 100644
index 000000000..2ff95c962
--- /dev/null
+++ b/elixir/apps/web/priv/static/.well-known/apple-developer-merchantid-domain-association
@@ -0,0 +1 @@
+7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313536363233343735303036312C227369676E6174757265223A22333038303036303932613836343838366637306430313037303261303830333038303032303130313331306633303064303630393630383634383031363530333034303230313035303033303830303630393261383634383836663730643031303730313030303061303830333038323033653333303832303338386130303330323031303230323038346333303431343935313964353433363330306130363038326138363438636533643034303330323330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313339333033353331333833303331333333323335333735613137306433323334333033353331333633303331333333323335333735613330356633313235333032333036303335353034303330633163363536333633326437333664373032643632373236663662363537323264373336393637366535663535343333343264353035323466343433313134333031323036303335353034306230633062363934663533323035333739373337343635366437333331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034633231353737656465626436633762323231386636386464373039306131323138646337623062643666326332383364383436303935643934616634613534313162383334323065643831316633343037653833333331663163353463336637656233323230643662616435643465666634393238393839336537633066313361333832303231313330383230323064333030633036303335353164313330313031666630343032333030303330316630363033353531643233303431383330313638303134323366323439633434663933653465663237653663346636323836633366613262626664326534623330343530363038326230363031303530353037303130313034333933303337333033353036303832623036303130353035303733303031383632393638373437343730336132663266366636333733373032653631373037303663363532653633366636643266366636333733373033303334326436313730373036633635363136393633363133333330333233303832303131643036303335353164323030343832303131343330383230313130333038323031306330363039326138363438383666373633363430353031333038316665333038316333303630383262303630313035303530373032303233303831623630633831623335323635366336393631366536333635323036663665323037343638363937333230363336353732373436393636363936333631373436353230363237393230363136653739323037303631373237343739323036313733373337353664363537333230363136333633363537303734363136653633363532303666363632303734363836353230373436383635366532303631373037303663363936333631363236633635323037333734363136653634363137323634323037343635373236643733323036313665363432303633366636653634363937343639366636653733323036663636323037353733363532633230363336353732373436393636363936333631373436353230373036663663363936333739323036313665363432303633363537323734363936363639363336313734363936663665323037303732363136333734363936333635323037333734363137343635366436353665373437333265333033363036303832623036303130353035303730323031313632613638373437343730336132663266373737373737326536313730373036633635326536333666366432663633363537323734363936363639363336313734363536313735373436383666373236393734373932663330333430363033353531643166303432643330326233303239613032376130323538363233363837343734373033613266326636333732366332653631373037303663363532653633366636643266363137303730366336353631363936333631333332653633373236633330316430363033353531643065303431363034313439343537646236666435373438313836383938393736326637653537383530376537396235383234333030653036303335353164306630313031666630343034303330323037383033303066303630393261383634383836663736333634303631643034303230353030333030613036303832613836343863653364303430333032303334393030333034363032323130306265303935373166653731653165373335623535653561666163623463373266656234343566333031383532323263373235313030326236316562643666353530323231303064313862333530613564643664643665623137343630333562313165623263653837636661336536616636636264383338303839306463383263646461613633333038323032656533303832303237356130303330323031303230323038343936643266626633613938646139373330306130363038326138363438636533643034303330323330363733313162333031393036303335353034303330633132343137303730366336353230353236663666373432303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330316531373064333133343330333533303336333233333334333633333330356131373064333233393330333533303336333233333334333633333330356133303761333132653330326330363033353530343033306332353431373037303663363532303431373037303663363936333631373436393666366532303439366537343635363737323631373436393666366532303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330353933303133303630373261383634386365336430323031303630383261383634386365336430333031303730333432303030346630313731313834313964373634383564353161356532353831303737366538383061326566646537626165346465303864666334623933653133333536643536363562333561653232643039373736306432323465376262613038666437363137636538386362373662623636373062656338653832393834666635343435613338316637333038316634333034363036303832623036303130353035303730313031303433613330333833303336303630383262303630313035303530373330303138363261363837343734373033613266326636663633373337303265363137303730366336353265363336663664326636663633373337303330333432643631373037303663363537323666366637343633363136373333333031643036303335353164306530343136303431343233663234396334346639336534656632376536633466363238366333666132626266643265346233303066303630333535316431333031303166663034303533303033303130316666333031663036303335353164323330343138333031363830313462626230646561313538333338383961613438613939646562656264656261666461636232346162333033373036303335353164316630343330333032653330326361303261613032383836323636383734373437303361326632663633373236633265363137303730366336353265363336663664326636313730373036633635373236663666373436333631363733333265363337323663333030653036303335353164306630313031666630343034303330323031303633303130303630613261383634383836663736333634303630323065303430323035303033303061303630383261383634386365336430343033303230333637303033303634303233303361636637323833353131363939623138366662333563333536636136326266663431376564643930663735346461323865626566313963383135653432623738396638393866373962353939663938643534313064386639646539633266653032333033323264643534343231623061333035373736633564663333383362393036376664313737633263323136643936346663363732363938323132366635346638376137643162393963623962303938393231363130363939306630393932316430303030333138323031386233303832303138373032303130313330383138363330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533303230383463333034313439353139643534333633303064303630393630383634383031363530333034303230313035303061303831393533303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333933303338333133393331333733313332333333303561333032613036303932613836343838366637306430313039333433313164333031623330306430363039363038363438303136353033303430323031303530306131306130363038326138363438636533643034303330323330326630363039326138363438383666373064303130393034333132323034323062303731303365313430613462386231376262613230316130336163643036396234653431366232613263383066383661383338313435633239373566633131333030613036303832613836343863653364303430333032303434363330343430323230343639306264636637626461663833636466343934396534633035313039656463663334373665303564373261313264376335666538633033303033343464663032323032363764353863393365626233353031333836363062353730373938613064643731313734316262353864626436613138363633353038353431656565393035303030303030303030303030227D
\ No newline at end of file
diff --git a/elixir/apps/web/test/web/live/actors/service_accounts/new_test.exs b/elixir/apps/web/test/web/live/actors/service_accounts/new_test.exs
index 21f270f0b..0bacab59e 100644
--- a/elixir/apps/web/test/web/live/actors/service_accounts/new_test.exs
+++ b/elixir/apps/web/test/web/live/actors/service_accounts/new_test.exs
@@ -118,6 +118,38 @@ defmodule Web.Live.Actors.ServiceAccount.NewTest do
}
end
+ test "renders changeset errors when seats count is exceeded", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ {:ok, account} =
+ Domain.Accounts.update_account(account, %{
+ limits: %{
+ service_accounts_count: 1
+ }
+ })
+
+ Fixtures.Actors.create_actor(type: :service_account, account: account)
+
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/actors/service_accounts/new")
+
+ attrs =
+ Fixtures.Actors.actor_attrs()
+ |> Map.take([:name])
+
+ html =
+ lv
+ |> form("form", actor: attrs)
+ |> render_submit()
+
+ assert html =~ "You have reached the maximum number of"
+ assert html =~ "service accounts allowed by your subscription plan"
+ end
+
test "creates a new actor on valid attrs", %{
account: account,
actor: actor,
diff --git a/elixir/apps/web/test/web/live/actors/users/new_test.exs b/elixir/apps/web/test/web/live/actors/users/new_test.exs
index 77b0ea4ee..5df1ba152 100644
--- a/elixir/apps/web/test/web/live/actors/users/new_test.exs
+++ b/elixir/apps/web/test/web/live/actors/users/new_test.exs
@@ -120,6 +120,39 @@ defmodule Web.Live.Actors.User.NewTest do
}
end
+ test "renders changeset errors when seats count is exceeded", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ {:ok, account} =
+ Domain.Accounts.update_account(account, %{
+ limits: %{
+ monthly_active_users_count: 1
+ }
+ })
+
+ actor = Fixtures.Actors.create_actor(type: :account_user, account: account)
+ Fixtures.Clients.create_client(account: account, actor: actor)
+
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/actors/users/new")
+
+ attrs =
+ Fixtures.Actors.actor_attrs()
+ |> Map.take([:name])
+
+ html =
+ lv
+ |> form("form", actor: attrs)
+ |> render_submit()
+
+ assert html =~ "You have reached the maximum number of"
+ assert html =~ "seats allowed by your subscription plan."
+ end
+
test "creates a new actor on valid attrs", %{
account: account,
actor: actor,
diff --git a/elixir/apps/web/test/web/live/settings/account/index_test.exs b/elixir/apps/web/test/web/live/settings/account/index_test.exs
deleted file mode 100644
index 547c60ce7..000000000
--- a/elixir/apps/web/test/web/live/settings/account/index_test.exs
+++ /dev/null
@@ -1,63 +0,0 @@
-defmodule Web.Live.Settings.Account.IndexTest do
- use Web.ConnCase, async: true
-
- setup do
- Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
-
- account = Fixtures.Accounts.create_account()
- 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"
-
- 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")
-
- assert item = Floki.find(html, "[aria-label='Breadcrumb']")
- breadcrumbs = String.trim(Floki.text(item))
- assert breadcrumbs =~ "Account Settings"
- end
-
- test "renders table with account information", %{
- account: account,
- identity: identity,
- conn: conn
- } do
- {:ok, lv, _html} =
- conn
- |> authorize_conn(identity)
- |> live(~p"/#{account}/settings/account")
-
- rows =
- lv
- |> element("#account")
- |> render()
- |> vertical_table_to_map()
-
- assert rows["account name"] == account.name
- assert rows["account id"] == account.id
- assert rows["account slug"] =~ account.slug
- 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
new file mode 100644
index 000000000..fcfc2628d
--- /dev/null
+++ b/elixir/apps/web/test/web/live/settings/account_test.exs
@@ -0,0 +1,124 @@
+defmodule Web.Live.Settings.AccountTest 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"
+
+ 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")
+
+ assert item = Floki.find(html, "[aria-label='Breadcrumb']")
+ breadcrumbs = String.trim(Floki.text(item))
+ assert breadcrumbs =~ "Account Settings"
+ end
+
+ test "renders table with account information even if billing portal is down", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/account")
+
+ rows =
+ lv
+ |> element("#account")
+ |> render()
+ |> vertical_table_to_map()
+
+ assert rows["account name"] == account.name
+ assert rows["account id"] == account.id
+ assert rows["account slug"] =~ account.slug
+ end
+
+ test "renders error when limit is exceeded", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ account =
+ Fixtures.Accounts.update_account(account, %{
+ warning: "You have reached your monthly active actors limit."
+ })
+
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/account")
+
+ html = lv |> render()
+ assert html =~ "You have reached your monthly active actors limit."
+ assert html =~ "check your billing information"
+ end
+
+ test "renders error when account is disabled", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ account =
+ Fixtures.Accounts.update_account(account, %{
+ disabled_at: DateTime.utc_now()
+ })
+
+ actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user)
+
+ Fixtures.Clients.create_client(
+ account: account,
+ actor: actor,
+ last_seen_at: DateTime.utc_now()
+ )
+
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/account")
+
+ html = lv |> render()
+ assert html =~ "This account has been disabled."
+ assert html =~ "contact support"
+ end
+end
diff --git a/elixir/apps/web/test/web/live/settings/billing_test.exs b/elixir/apps/web/test/web/live/settings/billing_test.exs
new file mode 100644
index 000000000..9b70bda9d
--- /dev/null
+++ b/elixir/apps/web/test/web/live/settings/billing_test.exs
@@ -0,0 +1,153 @@
+defmodule Web.Live.Settings.BillingTest 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,
+ service_accounts_count: 100,
+ gateway_groups_count: 10,
+ account_admin_users_count: 2
+ }
+ )
+
+ 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/billing"
+
+ 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/billing")
+
+ assert item = Floki.find(html, "[aria-label='Breadcrumb']")
+ breadcrumbs = String.trim(Floki.text(item))
+ assert breadcrumbs =~ "Billing"
+ end
+
+ test "renders table with account information even if billing portal is down", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/billing")
+
+ rows =
+ lv
+ |> element("#billing")
+ |> render()
+ |> vertical_table_to_map()
+
+ assert rows["current plan"] =~ account.metadata.stripe.product_name
+ assert rows["seats"] =~ "0 used / 100 allowed"
+ assert rows["sites"] =~ "0 used / 10 allowed"
+ assert rows["admins"] =~ "1 used / 2 allowed"
+
+ html = element(lv, "button[phx-click='redirect_to_billing_portal']") |> render_click()
+ assert html =~ "Billing portal is temporarily unavailable, please try again later."
+ end
+
+ test "renders billing portal button", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ Bypass.open()
+ |> Mocks.Stripe.mock_create_billing_session_endpoint(account)
+
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/billing")
+
+ assert has_element?(lv, "button[phx-click='redirect_to_billing_portal']")
+
+ assert {:error, {:redirect, %{to: to}}} =
+ element(lv, "button[phx-click='redirect_to_billing_portal']")
+ |> render_click()
+
+ assert to =~ "https://billing.stripe.com/p/session"
+ end
+
+ test "renders error when limit is exceeded", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ account =
+ Fixtures.Accounts.update_account(account, %{
+ warning: "You have reached your monthly active actors limit."
+ })
+
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/billing")
+
+ html = lv |> render()
+ assert html =~ "You have reached your monthly active actors limit."
+ assert html =~ "check your billing information"
+ end
+
+ test "renders error when account is disabled", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ account =
+ Fixtures.Accounts.update_account(account, %{
+ disabled_at: DateTime.utc_now()
+ })
+
+ actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user)
+
+ Fixtures.Clients.create_client(
+ account: account,
+ actor: actor,
+ last_seen_at: DateTime.utc_now()
+ )
+
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/settings/billing")
+
+ html = lv |> render()
+ assert html =~ "This account has been disabled."
+ assert html =~ "contact support"
+ end
+end
diff --git a/elixir/apps/web/test/web/live/settings/dns/index_test.exs b/elixir/apps/web/test/web/live/settings/dns_test.exs
similarity index 51%
rename from elixir/apps/web/test/web/live/settings/dns/index_test.exs
rename to elixir/apps/web/test/web/live/settings/dns_test.exs
index b880456bd..7dc909585 100644
--- a/elixir/apps/web/test/web/live/settings/dns/index_test.exs
+++ b/elixir/apps/web/test/web/live/settings/dns_test.exs
@@ -1,4 +1,4 @@
-defmodule Web.Live.Settings.DNS.IndexTest do
+defmodule Web.Live.Settings.DNSTest do
use Web.ConnCase, async: true
setup do
@@ -45,6 +45,8 @@ defmodule Web.Live.Settings.DNS.IndexTest do
identity: identity,
conn: conn
} do
+ Fixtures.Accounts.update_account(account, %{config: %{clients_upstream_dns: []}})
+
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
@@ -53,9 +55,10 @@ defmodule Web.Live.Settings.DNS.IndexTest do
form = lv |> form("form")
assert find_inputs(form) == [
- "configuration[clients_upstream_dns][0][_persistent_id]",
- "configuration[clients_upstream_dns][0][address]",
- "configuration[clients_upstream_dns][0][protocol]"
+ "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]"
]
end
@@ -64,7 +67,15 @@ defmodule Web.Live.Settings.DNS.IndexTest do
identity: identity,
conn: conn
} do
- attrs = %{configuration: %{clients_upstream_dns: %{"0" => %{address: "8.8.8.8"}}}}
+ Fixtures.Accounts.update_account(account, %{config: %{clients_upstream_dns: []}})
+
+ attrs = %{
+ account: %{
+ config: %{
+ clients_upstream_dns: %{"0" => %{address: "8.8.8.8"}}
+ }
+ }
+ }
{:ok, lv, _html} =
conn
@@ -78,12 +89,13 @@ defmodule Web.Live.Settings.DNS.IndexTest do
assert lv
|> form("form")
|> find_inputs() == [
- "configuration[clients_upstream_dns][0][_persistent_id]",
- "configuration[clients_upstream_dns][0][address]",
- "configuration[clients_upstream_dns][0][protocol]",
- "configuration[clients_upstream_dns][1][_persistent_id]",
- "configuration[clients_upstream_dns][1][address]",
- "configuration[clients_upstream_dns][1][protocol]"
+ "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][1][_persistent_id]",
+ "account[config][clients_upstream_dns][1][address]",
+ "account[config][clients_upstream_dns][1][protocol]"
]
end
@@ -93,8 +105,12 @@ defmodule Web.Live.Settings.DNS.IndexTest do
conn: conn
} do
attrs = %{
- configuration: %{
- clients_upstream_dns: %{"0" => %{address: "8.8.8.8"}}
+ account: %{
+ config: %{
+ clients_upstream_dns: %{
+ "0" => %{address: ""}
+ }
+ }
}
}
@@ -110,28 +126,16 @@ defmodule Web.Live.Settings.DNS.IndexTest do
assert lv
|> form("form")
|> find_inputs() == [
- "configuration[clients_upstream_dns][0][_persistent_id]",
- "configuration[clients_upstream_dns][0][address]",
- "configuration[clients_upstream_dns][0][protocol]",
- "configuration[clients_upstream_dns][1][_persistent_id]",
- "configuration[clients_upstream_dns][1][address]",
- "configuration[clients_upstream_dns][1][protocol]"
- ]
-
- empty_attrs = %{
- configuration: %{
- clients_upstream_dns: %{"0" => %{address: ""}}
- }
- }
-
- lv |> form("form", empty_attrs) |> render_submit()
-
- assert lv
- |> form("form")
- |> find_inputs() == [
- "configuration[clients_upstream_dns][0][_persistent_id]",
- "configuration[clients_upstream_dns][0][address]",
- "configuration[clients_upstream_dns][0][protocol]"
+ "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][1][_persistent_id]",
+ "account[config][clients_upstream_dns][1][address]",
+ "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]"
]
end
@@ -145,8 +149,10 @@ defmodule Web.Live.Settings.DNS.IndexTest do
addr2 = %{address: "1.1.1.1"}
attrs = %{
- configuration: %{
- clients_upstream_dns: %{"0" => addr1}
+ account: %{
+ config: %{
+ clients_upstream_dns: %{"0" => addr1}
+ }
}
}
@@ -160,16 +166,28 @@ defmodule Web.Live.Settings.DNS.IndexTest do
|> render_submit()
assert lv
- |> form("form", %{configuration: %{clients_upstream_dns: %{"1" => addr1}}})
- |> render_change() =~ "no duplicates allowed"
+ |> form("form", %{
+ account: %{
+ config: %{clients_upstream_dns: %{"1" => addr1}}
+ }
+ })
+ |> render_change() =~ "all addresses must be unique"
refute lv
- |> form("form", %{configuration: %{clients_upstream_dns: %{"1" => addr2}}})
- |> render_change() =~ "no duplicates allowed"
+ |> form("form", %{
+ account: %{
+ config: %{clients_upstream_dns: %{"1" => addr2}}
+ }
+ })
+ |> render_change() =~ "all addresses must be unique"
assert lv
- |> form("form", %{configuration: %{clients_upstream_dns: %{"1" => addr1_dup}}})
- |> render_change() =~ "no duplicates allowed"
+ |> form("form", %{
+ account: %{
+ config: %{clients_upstream_dns: %{"1" => addr1_dup}}
+ }
+ })
+ |> render_change() =~ "all addresses must be unique"
end
test "does not display 'cannot be empty' error message", %{
@@ -178,8 +196,10 @@ defmodule Web.Live.Settings.DNS.IndexTest do
conn: conn
} do
attrs = %{
- configuration: %{
- clients_upstream_dns: %{"0" => %{address: "8.8.8.8"}}
+ account: %{
+ config: %{
+ clients_upstream_dns: %{"0" => %{address: "8.8.8.8"}}
+ }
}
}
@@ -193,7 +213,13 @@ defmodule Web.Live.Settings.DNS.IndexTest do
|> render_submit()
refute lv
- |> form("form", %{configuration: %{clients_upstream_dns: %{"0" => %{address: ""}}}})
+ |> form("form", %{
+ account: %{
+ config: %{
+ clients_upstream_dns: %{"0" => %{address: ""}}
+ }
+ }
+ })
|> render_change() =~ "can't be blank"
end
end
diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/new_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/new_test.exs
index 4b6568220..ca24ff5ad 100644
--- a/elixir/apps/web/test/web/live/settings/identity_providers/new_test.exs
+++ b/elixir/apps/web/test/web/live/settings/identity_providers/new_test.exs
@@ -4,7 +4,7 @@ defmodule Web.Live.Settings.IdentityProviders.NewTest do
setup do
Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
- account = Fixtures.Accounts.create_account()
+ account = Fixtures.Accounts.create_account(features: %{idp_sync: false})
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
{provider, bypass} = Fixtures.Auth.start_and_create_openid_connect_provider(account: account)
@@ -44,8 +44,8 @@ defmodule Web.Live.Settings.IdentityProviders.NewTest do
assert has_element?(lv, "#idp-option-google_workspace")
assert html =~ "Google Workspace"
- assert html =~ "Feature available on the Enterprise plan"
- assert html =~ "ENTERPRISE"
+ assert html =~ "Feature available on a higher pricing plan"
+ assert html =~ "UPGRADE TO UNLOCK"
assert has_element?(lv, "#idp-option-microsoft_entra")
assert html =~ "Microsoft Entra"
diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/system/show_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/system/show_test.exs
index 0b260710a..bc38afdc5 100644
--- a/elixir/apps/web/test/web/live/settings/identity_providers/system/show_test.exs
+++ b/elixir/apps/web/test/web/live/settings/identity_providers/system/show_test.exs
@@ -138,7 +138,7 @@ defmodule Web.Live.Settings.IdentityProviders.System.ShowTest do
|> Map.fetch!("status") == "Active"
end
- test "allows deleting identity providers", %{
+ test "does not allow deleting system identity providers", %{
account: account,
provider: provider,
identity: identity,
@@ -149,12 +149,6 @@ defmodule Web.Live.Settings.IdentityProviders.System.ShowTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/settings/identity_providers/system/#{provider}")
- lv
- |> element("button", "Delete Identity Provider")
- |> render_click()
-
- assert_redirected(lv, ~p"/#{account}/settings/identity_providers")
-
- assert Repo.get(Domain.Auth.Provider, provider.id).deleted_at
+ refute has_element?(lv, "button", "Delete Identity Provider")
end
end
diff --git a/elixir/apps/web/test/web/live/sidebar_test.exs b/elixir/apps/web/test/web/live/sidebar_test.exs
index e6fc76171..5a027ad23 100644
--- a/elixir/apps/web/test/web/live/sidebar_test.exs
+++ b/elixir/apps/web/test/web/live/sidebar_test.exs
@@ -18,7 +18,7 @@ defmodule Web.SidebarTest do
account: account,
identity: identity
} do
- {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/actors")
+ {:ok, _lv, html} = conn |> authorize_conn(identity) |> live(~p"/#{account}/actors")
refute Enum.empty?(Floki.find(html, "ul#dropdown-settings.hidden"))
end
@@ -27,7 +27,7 @@ defmodule Web.SidebarTest do
account: account,
identity: identity
} do
- {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/settings/dns")
+ {:ok, _lv, html} = conn |> authorize_conn(identity) |> live(~p"/#{account}/settings/dns")
assert Enum.empty?(Floki.find(html, "ul#dropdown-settings.hidden"))
refute Enum.empty?(Floki.find(html, "ul#dropdown-settings"))
end
@@ -37,7 +37,7 @@ defmodule Web.SidebarTest do
identity: identity,
conn: conn
} do
- {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/actors")
+ {:ok, _lv, html} = conn |> authorize_conn(identity) |> live(~p"/#{account}/actors")
assert item = Floki.find(html, "a.bg-neutral-100[href='/#{account.slug}/actors']")
assert String.trim(Floki.text(item)) == "Actors"
end
@@ -47,7 +47,7 @@ defmodule Web.SidebarTest do
identity: identity,
conn: conn
} do
- {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/groups")
+ {:ok, _lv, html} = conn |> authorize_conn(identity) |> live(~p"/#{account}/groups")
assert item = Floki.find(html, "a.bg-neutral-100[href='/#{account.slug}/groups']")
assert String.trim(Floki.text(item)) == "Groups"
end
@@ -57,7 +57,7 @@ defmodule Web.SidebarTest do
identity: identity,
conn: conn
} do
- {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/clients")
+ {:ok, _lv, html} = conn |> authorize_conn(identity) |> live(~p"/#{account}/clients")
assert item = Floki.find(html, "a.bg-neutral-100[href='/#{account.slug}/clients']")
assert String.trim(Floki.text(item)) == "Clients"
end
@@ -67,7 +67,7 @@ defmodule Web.SidebarTest do
identity: identity,
conn: conn
} do
- {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/sites")
+ {:ok, _lv, html} = conn |> authorize_conn(identity) |> live(~p"/#{account}/sites")
assert item = Floki.find(html, "a.bg-neutral-100[href='/#{account.slug}/sites']")
assert String.trim(Floki.text(item)) == "Sites"
end
@@ -77,7 +77,7 @@ defmodule Web.SidebarTest do
identity: identity,
conn: conn
} do
- {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/relay_groups")
+ {:ok, _lv, html} = conn |> authorize_conn(identity) |> live(~p"/#{account}/relay_groups")
assert item = Floki.find(html, "a.bg-neutral-100[href='/#{account.slug}/relay_groups']")
assert String.trim(Floki.text(item)) == "Relays"
end
@@ -87,7 +87,7 @@ defmodule Web.SidebarTest do
# identity: identity,
# conn: conn
# } do
- # {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/resources")
+ # {:ok, _lv, html} = conn |> authorize_conn(identity) |> live( ~p"/#{account}/resources")
# assert item = Floki.find(html, "a.bg-neutral-100[href='/#{account.slug}/resources']")
# assert String.trim(Floki.text(item)) == "Resources"
# end
@@ -97,7 +97,7 @@ defmodule Web.SidebarTest do
identity: identity,
conn: conn
} do
- {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/policies")
+ {:ok, _lv, html} = conn |> authorize_conn(identity) |> live(~p"/#{account}/policies")
assert item = Floki.find(html, "a.bg-neutral-100[href='/#{account.slug}/policies']")
assert String.trim(Floki.text(item)) == "Policies"
end
@@ -107,7 +107,7 @@ defmodule Web.SidebarTest do
identity: identity,
conn: conn
} do
- {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/settings/account")
+ {:ok, _lv, html} = conn |> authorize_conn(identity) |> live(~p"/#{account}/settings/account")
assert item = Floki.find(html, "a.bg-neutral-100[href='/#{account.slug}/settings/account']")
assert String.trim(Floki.text(item)) == "Account"
end
@@ -118,7 +118,7 @@ defmodule Web.SidebarTest do
conn: conn
} do
{:ok, _lv, html} =
- live(authorize_conn(conn, identity), ~p"/#{account}/settings/identity_providers")
+ conn |> authorize_conn(identity) |> live(~p"/#{account}/settings/identity_providers")
assert item =
Floki.find(
@@ -154,7 +154,7 @@ defmodule Web.SidebarTest do
identity: identity,
conn: conn
} do
- {:ok, _lv, html} = live(authorize_conn(conn, identity), ~p"/#{account}/settings/dns")
+ {:ok, _lv, html} = conn |> authorize_conn(identity) |> live(~p"/#{account}/settings/dns")
assert item = Floki.find(html, "a.bg-neutral-100[href='/#{account.slug}/settings/dns']")
assert String.trim(Floki.text(item)) == "DNS"
end
diff --git a/elixir/apps/web/test/web/live/sign_in_test.exs b/elixir/apps/web/test/web/live/sign_in_test.exs
index ae69ad48f..8c44ca954 100644
--- a/elixir/apps/web/test/web/live/sign_in_test.exs
+++ b/elixir/apps/web/test/web/live/sign_in_test.exs
@@ -68,4 +68,13 @@ defmodule Web.SignInTest do
refute html =~ ~s|Meant to sign in from a client instead?|
end
+
+ test "renders error when account is disabled", %{conn: conn} do
+ Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
+ account = Fixtures.Accounts.create_account()
+ {:ok, _account} = Domain.Accounts.update_account(account, %{disabled_at: DateTime.utc_now()})
+ Fixtures.Auth.create_email_provider(account: account)
+ {:ok, _lv, html} = live(conn, ~p"/#{account}")
+ assert html =~ "This account has been disabled, please contact your administrator."
+ end
end
diff --git a/elixir/apps/web/test/web/live/sign_up_test.exs b/elixir/apps/web/test/web/live/sign_up_test.exs
index 86befb485..dd989e6b3 100644
--- a/elixir/apps/web/test/web/live/sign_up_test.exs
+++ b/elixir/apps/web/test/web/live/sign_up_test.exs
@@ -32,6 +32,13 @@ defmodule Web.Live.SignUpTest do
email: email
}
+ Bypass.open()
+ |> Domain.Mocks.Stripe.mock_create_customer_endpoint(%{
+ id: Ecto.UUID.generate(),
+ name: account_name
+ })
+ |> Domain.Mocks.Stripe.mock_create_subscription_endpoint()
+
assert html =
lv
|> form("form", registration: attrs)
@@ -42,6 +49,7 @@ defmodule Web.Live.SignUpTest do
account = Repo.one(Domain.Accounts.Account)
assert account.name == account_name
+ assert account.metadata.stripe.customer_id
provider = Repo.one(Domain.Auth.Provider)
assert provider.account_id == account.id
diff --git a/elixir/apps/web/test/web/live/sites/new_test.exs b/elixir/apps/web/test/web/live/sites/new_test.exs
index a5e6b7efe..b95214123 100644
--- a/elixir/apps/web/test/web/live/sites/new_test.exs
+++ b/elixir/apps/web/test/web/live/sites/new_test.exs
@@ -126,4 +126,36 @@ defmodule Web.Live.Sites.NewTest do
assert assert_redirect(lv, ~p"/#{account}/sites/#{group}")
end
+
+ test "renders error when sites limit is reached", %{
+ account: account,
+ identity: identity,
+ conn: conn
+ } do
+ Fixtures.Gateways.create_group(account: account)
+
+ {:ok, account} =
+ Domain.Accounts.update_account(account, %{
+ limits: %{
+ gateway_groups_count: 1
+ }
+ })
+
+ {:ok, lv, _html} =
+ conn
+ |> authorize_conn(identity)
+ |> live(~p"/#{account}/sites/new")
+
+ attrs =
+ Fixtures.Gateways.group_attrs()
+ |> Map.take([:name])
+
+ html =
+ lv
+ |> form("form", group: attrs)
+ |> render_submit()
+
+ assert html =~ "You have reached the maximum number of"
+ assert html =~ "sites allowed by your subscription plan."
+ end
end
diff --git a/elixir/config/config.exs b/elixir/config/config.exs
index 89c170d5a..1efccddc3 100644
--- a/elixir/config/config.exs
+++ b/elixir/config/config.exs
@@ -33,8 +33,6 @@ config :domain, Domain.Tokens,
key_base: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2",
salt: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2"
-config :domain, Domain.Clients, upstream_dns: ["1.1.1.1"]
-
config :domain, Domain.Gateways,
gateway_ipv4_masquerade: true,
gateway_ipv6_masquerade: true
@@ -53,6 +51,16 @@ config :domain, Domain.Auth.Adapters.MicrosoftEntra.APIClient,
config :domain, Domain.Auth.Adapters.Okta.APIClient, finch_transport_opts: []
+config :domain, Domain.Billing.Stripe.APIClient,
+ endpoint: "https://api.stripe.com",
+ finch_transport_opts: []
+
+config :domain, Domain.Billing,
+ enabled: true,
+ secret_key: "sk_test_1111",
+ webhook_signing_secret: "whsec_test_1111",
+ default_price_id: "price_1OkUIcADeNU9NGxvTNA4PPq6"
+
config :domain, platform_adapter: nil
config :domain, Domain.GoogleCloudPlatform,
@@ -72,6 +80,7 @@ config :domain, Domain.Instrumentation,
client_logs_bucket: "logs"
config :domain, :enabled_features,
+ idp_sync: true,
traffic_filters: true,
sign_up: true,
flow_activities: true,
@@ -123,6 +132,8 @@ config :web,
config :web, Web.Plugs.SecureHeaders,
csp_policy: [
"default-src 'self' 'nonce-${nonce}'",
+ "frame-src 'self' https://js.stripe.com",
+ "script-src 'self' https://js.stripe.com",
"img-src 'self' data: https://www.gravatar.com",
"style-src 'self' 'unsafe-inline'"
]
diff --git a/elixir/config/dev.exs b/elixir/config/dev.exs
index b85b8e4ce..5a4521478 100644
--- a/elixir/config/dev.exs
+++ b/elixir/config/dev.exs
@@ -13,6 +13,11 @@ config :domain, Domain.Repo,
config :domain, outbound_email_adapter_configured?: true
+config :domain, Domain.Billing,
+ enabled: System.get_env("BILLING_ENABLED", "false") == "true",
+ secret_key: System.get_env("STRIPE_SECRET_KEY", "sk_dev_1111"),
+ webhook_signing_secret: System.get_env("STRIPE_WEBHOOK_SIGNING_SECRET", "whsec_dev_1111")
+
###############################
##### Web #####################
###############################
@@ -62,7 +67,8 @@ config :web, Web.Plugs.SecureHeaders,
"default-src 'self' 'nonce-${nonce}' https://cdn.tailwindcss.com/",
"img-src 'self' data: https://www.gravatar.com",
"style-src 'self' 'unsafe-inline'",
- "script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com/"
+ "frame-src 'self' https://js.stripe.com",
+ "script-src 'self' 'unsafe-inline' https://js.stripe.com https://cdn.tailwindcss.com/"
]
# Note: on Linux you may need to add `--add-host=host.docker.internal:host-gateway`
diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs
index f19344a73..5cd932c78 100644
--- a/elixir/config/runtime.exs
+++ b/elixir/config/runtime.exs
@@ -31,8 +31,6 @@ if config_env() == :prod do
key_base: compile_config!(:tokens_key_base),
salt: compile_config!(:tokens_salt)
- config :domain, Domain.Clients, upstream_dns: compile_config!(:clients_upstream_dns)
-
config :domain, Domain.Gateways,
gateway_ipv4_masquerade: compile_config!(:gateway_ipv4_masquerade),
gateway_ipv6_masquerade: compile_config!(:gateway_ipv6_masquerade)
@@ -44,6 +42,16 @@ if config_env() == :prod do
config :domain, Domain.Auth.Adapters.GoogleWorkspace.APIClient,
finch_transport_opts: compile_config!(:http_client_ssl_opts)
+ config :domain, Domain.Billing.Stripe.APIClient,
+ endpoint: "https://api.stripe.com",
+ finch_transport_opts: []
+
+ config :domain, Domain.Billing,
+ enabled: compile_config!(:billing_enabled),
+ secret_key: compile_config!(:stripe_secret_key),
+ webhook_signing_secret: compile_config!(:stripe_webhook_signing_secret),
+ default_price_id: compile_config!(:stripe_default_price_id)
+
config :domain, platform_adapter: compile_config!(:platform_adapter)
if platform_adapter = compile_config!(:platform_adapter) do
@@ -59,6 +67,7 @@ if config_env() == :prod do
client_logs_bucket: compile_config!(:instrumentation_client_logs_bucket)
config :domain, :enabled_features,
+ idp_sync: compile_config!(:feature_idp_sync_enabled),
traffic_filters: compile_config!(:feature_traffic_filters_enabled),
sign_up: compile_config!(:feature_sign_up_enabled),
flow_activities: compile_config!(:feature_flow_activities_enabled),
diff --git a/elixir/config/test.exs b/elixir/config/test.exs
index 22fea885d..4cb4ee675 100644
--- a/elixir/config/test.exs
+++ b/elixir/config/test.exs
@@ -42,7 +42,8 @@ config :web, Web.Plugs.SecureHeaders,
"default-src 'self' 'nonce-${nonce}' https://cdn.tailwindcss.com/",
"img-src 'self' data: https://www.gravatar.com",
"style-src 'self' 'unsafe-inline'",
- "script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com/"
+ "frame-src 'self' https://js.stripe.com",
+ "script-src 'self' 'unsafe-inline' https://js.stripe.com https://cdn.tailwindcss.com/"
]
###############################
diff --git a/kotlin/android/README.md b/kotlin/android/README.md
index ac80ccf02..8f1fc4d46 100644
--- a/kotlin/android/README.md
+++ b/kotlin/android/README.md
@@ -63,14 +63,6 @@ locally.
1. Perform a test build: `./gradlew assembleDebug`.
-1. Add your debug signing key's SHA256 fingerprint to the portal's
- [`assetlinks.json`](../../elixir/apps/web/priv/static/.well-known/assetlinks.json)
- file. This is required for the App Links to successfully intercept the Auth
- redirect.
- ```
- ./gradlew signingReport
- ```
-
# Release Setup
We release from GitHub CI, so this shouldn't be necessary. But if you're looking
diff --git a/terraform/environments/production/main.tf b/terraform/environments/production/main.tf
index 4f8aa99ca..c3f932a35 100644
--- a/terraform/environments/production/main.tf
+++ b/terraform/environments/production/main.tf
@@ -463,6 +463,23 @@ locals {
name = "DOCKER_REGISTRY"
value = "ghcr.io/firezone"
},
+ # Billing system
+ {
+ name = "BILLING_ENABLED"
+ value = "true"
+ },
+ {
+ name = "STRIPE_SECRET_KEY"
+ value = var.stripe_secret_key
+ },
+ {
+ name = "STRIPE_WEBHOOK_SIGNING_SECRET"
+ value = var.stripe_webhook_signing_secret
+ },
+ {
+ name = "STRIPE_DEFAULT_PRICE_ID"
+ value = var.stripe_default_price_id
+ },
# Telemetry
{
name = "TELEMETRY_ENABLED"
diff --git a/terraform/environments/production/variables.tf b/terraform/environments/production/variables.tf
index 0ddb1e539..8305f1ef9 100644
--- a/terraform/environments/production/variables.tf
+++ b/terraform/environments/production/variables.tf
@@ -9,13 +9,15 @@ variable "metabase_image_tag" {
}
variable "relay_token" {
- type = string
- default = null
+ type = string
+ default = null
+ sensitive = true
}
variable "gateway_token" {
- type = string
- default = null
+ type = string
+ default = null
+ sensitive = true
}
variable "slack_alerts_channel" {
@@ -27,16 +29,34 @@ variable "slack_alerts_channel" {
variable "slack_alerts_auth_token" {
type = string
description = "Slack auth token for the infra alerts channel"
+ sensitive = true
}
variable "postmark_server_api_token" {
- type = string
+ type = string
+ sensitive = true
}
variable "mailgun_server_api_token" {
- type = string
+ type = string
+ sensitive = true
}
variable "pagerduty_auth_token" {
+ type = string
+ sensitive = true
+}
+
+variable "stripe_secret_key" {
+ type = string
+ sensitive = true
+}
+
+variable "stripe_webhook_signing_secret" {
+ type = string
+ sensitive = true
+}
+
+variable "stripe_default_price_id" {
type = string
}
diff --git a/terraform/environments/staging/main.tf b/terraform/environments/staging/main.tf
index 8b9f7a4cf..e2bd3ec38 100644
--- a/terraform/environments/staging/main.tf
+++ b/terraform/environments/staging/main.tf
@@ -415,6 +415,23 @@ locals {
name = "DOCKER_REGISTRY"
value = "${module.google-artifact-registry.url}/${module.google-artifact-registry.repo}"
},
+ # Billing system
+ {
+ name = "BILLING_ENABLED"
+ value = "true"
+ },
+ {
+ name = "STRIPE_SECRET_KEY"
+ value = var.stripe_secret_key
+ },
+ {
+ name = "STRIPE_WEBHOOK_SIGNING_SECRET"
+ value = var.stripe_webhook_signing_secret
+ },
+ {
+ name = "STRIPE_DEFAULT_PRICE_ID"
+ value = var.stripe_default_price_id
+ },
# Telemetry
{
name = "TELEMETRY_ENABLED"
diff --git a/terraform/environments/staging/variables.tf b/terraform/environments/staging/variables.tf
index 0cf8428a7..450e80502 100644
--- a/terraform/environments/staging/variables.tf
+++ b/terraform/environments/staging/variables.tf
@@ -37,3 +37,17 @@ variable "postmark_server_api_token" {
variable "mailgun_server_api_token" {
type = string
}
+
+variable "stripe_secret_key" {
+ type = string
+ sensitive = true
+}
+
+variable "stripe_webhook_signing_secret" {
+ type = string
+ sensitive = true
+}
+
+variable "stripe_default_price_id" {
+ type = string
+}