diff --git a/elixir/apps/web/lib/web/components/layouts/app.html.heex b/elixir/apps/web/lib/web/components/layouts/app.html.heex index c37bb1650..5fd597446 100644 --- a/elixir/apps/web/lib/web/components/layouts/app.html.heex +++ b/elixir/apps/web/lib/web/components/layouts/app.html.heex @@ -72,10 +72,7 @@ Identity Providers <:item navigate={~p"/#{@account}/settings/dns"}>DNS - <:item - :if={Domain.Accounts.rest_api_enabled?(@account)} - navigate={~p"/#{@account}/settings/api_clients"} - > + <:item navigate={~p"/#{@account}/settings/api_clients"}> API Clients diff --git a/elixir/apps/web/lib/web/live/settings/api_clients/beta.ex b/elixir/apps/web/lib/web/live/settings/api_clients/beta.ex new file mode 100644 index 000000000..4380a7c18 --- /dev/null +++ b/elixir/apps/web/lib/web/live/settings/api_clients/beta.ex @@ -0,0 +1,73 @@ +defmodule Web.Settings.ApiClients.Beta do + use Web, :live_view + + def mount(_params, _session, socket) do + if Domain.Accounts.rest_api_enabled?(socket.assigns.account) do + {:ok, push_navigate(socket, to: ~p"/#{socket.assigns.account}/settings/api_clients")} + else + socket = + socket + |> assign(:page_title, "API Clients") + |> assign(:requested, false) + + {:ok, socket} + end + end + + def render(assigns) do + ~H""" + <.breadcrumbs account={@account}> + <.breadcrumb path={~p"/#{@account}/settings/api_clients"}>API Clients + <.breadcrumb path={~p"/#{@account}/settings/api_clients/beta"}>Beta + + + <.section> + <:title><%= @page_title %> + <:help> + API Clients are used to manage Firezone configuration through a REST API. See our + interactive API docs + + <:content> + <.flash kind={:info}> +
+ REST API Beta +
+ The REST API is currently in closed beta. + ++ + Click here + + to request access. +
+ + ++ Your request to join the closed beta has been made. +
+ + + + + """ + end + + def handle_event("request_access", _params, socket) do + Web.Mailer.BetaEmail.rest_api_beta_email( + socket.assigns.account, + socket.assigns.subject + ) + |> Web.Mailer.deliver() + + socket = + socket + |> assign(:requested, true) + + {:noreply, socket} + end +end diff --git a/elixir/apps/web/lib/web/live/settings/api_clients/edit.ex b/elixir/apps/web/lib/web/live/settings/api_clients/edit.ex index 32f65a8ff..ff5d97579 100644 --- a/elixir/apps/web/lib/web/live/settings/api_clients/edit.ex +++ b/elixir/apps/web/lib/web/live/settings/api_clients/edit.ex @@ -4,23 +4,24 @@ defmodule Web.Settings.ApiClients.Edit do alias Domain.Actors def mount(%{"id" => id}, _session, socket) do - unless Domain.Config.global_feature_enabled?(:rest_api), - do: raise(Web.LiveErrors.NotFoundError) + if Domain.Accounts.rest_api_enabled?(socket.assigns.account) do + with {:ok, actor} <- Actors.fetch_actor_by_id(id, socket.assigns.subject, preload: []), + nil <- actor.deleted_at do + changeset = Actors.change_actor(actor) - with {:ok, actor} <- Actors.fetch_actor_by_id(id, socket.assigns.subject, preload: []), - nil <- actor.deleted_at do - changeset = Actors.change_actor(actor) + socket = + assign(socket, + actor: actor, + form: to_form(changeset), + page_title: "Edit #{actor.name}" + ) - socket = - assign(socket, - actor: actor, - form: to_form(changeset), - page_title: "Edit #{actor.name}" - ) - - {:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]} + {:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]} + else + _other -> raise Web.LiveErrors.NotFoundError + end else - _other -> raise Web.LiveErrors.NotFoundError + {:ok, push_navigate(socket, to: ~p"/#{socket.assigns.account}/settings/api_clients/beta")} end end diff --git a/elixir/apps/web/lib/web/live/settings/api_clients/index.ex b/elixir/apps/web/lib/web/live/settings/api_clients/index.ex index d964e71e3..c974f476e 100644 --- a/elixir/apps/web/lib/web/live/settings/api_clients/index.ex +++ b/elixir/apps/web/lib/web/live/settings/api_clients/index.ex @@ -3,28 +3,29 @@ defmodule Web.Settings.ApiClients.Index do alias Domain.Actors def mount(_params, _session, socket) do - unless Domain.Config.global_feature_enabled?(:rest_api), - do: raise(Web.LiveErrors.NotFoundError) + if Domain.Accounts.rest_api_enabled?(socket.assigns.account) do + socket = + socket + |> assign(page_title: "API Clients") + |> assign_live_table("actors", + query_module: Actors.Actor.Query, + sortable_fields: [ + {:actors, :name}, + {:actors, :status} + ], + enforce_filters: [ + {:type, "api_client"} + ], + hide_filters: [ + :provider_id + ], + callback: &handle_api_clients_update!/2 + ) - socket = - socket - |> assign(page_title: "API Clients") - |> assign_live_table("actors", - query_module: Actors.Actor.Query, - sortable_fields: [ - {:actors, :name}, - {:actors, :status} - ], - enforce_filters: [ - {:type, "api_client"} - ], - hide_filters: [ - :provider_id - ], - callback: &handle_api_clients_update!/2 - ) - - {:ok, socket} + {:ok, socket} + else + {:ok, push_navigate(socket, to: ~p"/#{socket.assigns.account}/settings/api_clients/beta")} + end end def handle_params(params, uri, socket) do diff --git a/elixir/apps/web/lib/web/live/settings/api_clients/new.ex b/elixir/apps/web/lib/web/live/settings/api_clients/new.ex index a96039569..2f2610526 100644 --- a/elixir/apps/web/lib/web/live/settings/api_clients/new.ex +++ b/elixir/apps/web/lib/web/live/settings/api_clients/new.ex @@ -4,18 +4,19 @@ defmodule Web.Settings.ApiClients.New do alias Domain.Actors def mount(_params, _session, socket) do - unless Domain.Config.global_feature_enabled?(:rest_api), - do: raise(Web.LiveErrors.NotFoundError) + if Domain.Accounts.rest_api_enabled?(socket.assigns.account) do + changeset = Actors.new_actor(%{type: :api_client}) - changeset = Actors.new_actor(%{type: :api_client}) + socket = + assign(socket, + form: to_form(changeset), + page_title: "New API Client" + ) - socket = - assign(socket, - form: to_form(changeset), - page_title: "New API Client" - ) - - {:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]} + {:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]} + else + {:ok, push_navigate(socket, to: ~p"/#{socket.assigns.account}/settings/api_clients/beta")} + end end def render(assigns) do diff --git a/elixir/apps/web/lib/web/live/settings/api_clients/show.ex b/elixir/apps/web/lib/web/live/settings/api_clients/show.ex index 9d1cfcb38..c981c6c51 100644 --- a/elixir/apps/web/lib/web/live/settings/api_clients/show.ex +++ b/elixir/apps/web/lib/web/live/settings/api_clients/show.ex @@ -3,23 +3,24 @@ defmodule Web.Settings.ApiClients.Show do alias Domain.{Actors, Tokens} def mount(%{"id" => id}, _session, socket) do - unless Domain.Config.global_feature_enabled?(:rest_api), - do: raise(Web.LiveErrors.NotFoundError) + if Domain.Accounts.rest_api_enabled?(socket.assigns.account) do + with {:ok, actor} <- Actors.fetch_actor_by_id(id, socket.assigns.subject, preload: []) do + socket = + socket + |> assign( + actor: actor, + page_title: "API Client #{actor.name}" + ) + |> assign_live_table("tokens", + query_module: Tokens.Token.Query, + sortable_fields: [], + callback: &handle_tokens_update!/2 + ) - with {:ok, actor} <- Actors.fetch_actor_by_id(id, socket.assigns.subject, preload: []) do - socket = - socket - |> assign( - actor: actor, - page_title: "API Client #{actor.name}" - ) - |> assign_live_table("tokens", - query_module: Tokens.Token.Query, - sortable_fields: [], - callback: &handle_tokens_update!/2 - ) - - {:ok, socket} + {:ok, socket} + end + else + {:ok, push_navigate(socket, to: ~p"/#{socket.assigns.account}/settings/api_clients/beta")} end end diff --git a/elixir/apps/web/lib/web/mailer.ex b/elixir/apps/web/lib/web/mailer.ex index b97373f8b..e6b708dac 100644 --- a/elixir/apps/web/lib/web/mailer.ex +++ b/elixir/apps/web/lib/web/mailer.ex @@ -72,6 +72,13 @@ defmodule Web.Mailer do |> Email.text_body(render_template(view, template, "text", assigns)) end + def render_text_body(%Swoosh.Email{} = email, view, template, assigns) do + assigns = assigns ++ [email: email] + + email + |> Email.text_body(render_template(view, template, "text", assigns)) + end + def active? do mailer_config = Domain.Config.fetch_env!(:web, Web.Mailer) mailer_config[:from_email] && mailer_config[:adapter] diff --git a/elixir/apps/web/lib/web/mailer/beta_email.ex b/elixir/apps/web/lib/web/mailer/beta_email.ex new file mode 100644 index 000000000..8cdd809dc --- /dev/null +++ b/elixir/apps/web/lib/web/mailer/beta_email.ex @@ -0,0 +1,39 @@ +defmodule Web.Mailer.BetaEmail do + use Web, :html + import Swoosh.Email + import Web.Mailer + + embed_templates "beta_email/*.text", suffix: "_text" + + def rest_api_beta_email( + %Domain.Accounts.Account{} = account, + %Domain.Auth.Subject{} = subject + ) do + default_email() + |> subject("REST API Beta Request - #{account.slug}") + |> to("support@firezone.dev") + |> render_text_body(__MODULE__, :rest_api_request, + account: account, + subject: subject + ) + end + + # def sign_up_link_email( + # %Domain.Accounts.Account{} = account, + # %Domain.Auth.Identity{} = identity, + # user_agent, + # remote_ip + # ) do + # sign_in_form_url = url(~p"/#{account}") + + # default_email() + # |> subject("Welcome to Firezone") + # |> to(identity.provider_identifier) + # |> render_body(__MODULE__, :sign_up_link, + # account: account, + # sign_in_form_url: sign_in_form_url, + # user_agent: user_agent, + # remote_ip: "#{:inet.ntoa(remote_ip)}" + # ) + # end +end diff --git a/elixir/apps/web/lib/web/mailer/beta_email/rest_api_request.text.heex b/elixir/apps/web/lib/web/mailer/beta_email/rest_api_request.text.heex new file mode 100644 index 000000000..33491b0ed --- /dev/null +++ b/elixir/apps/web/lib/web/mailer/beta_email/rest_api_request.text.heex @@ -0,0 +1,10 @@ +REST API Beta Request + +Request details: +---------------- +Account ID: <%= @account.id %> +Account Slug: <%= @account.slug %> +Account Name: <%= @account.name %> +Actor ID: <%= @subject.actor.id %> +Actor Name: <%= @subject.actor.name %> +Identifier: <%= @subject.identity.provider_identifier %> diff --git a/elixir/apps/web/lib/web/router.ex b/elixir/apps/web/lib/web/router.ex index c73effce1..a27e39ae1 100644 --- a/elixir/apps/web/lib/web/router.ex +++ b/elixir/apps/web/lib/web/router.ex @@ -209,6 +209,7 @@ defmodule Web.Router do live "/billing", Billing scope "/api_clients", ApiClients do + live "/beta", Beta live "/", Index live "/new", New live "/:id/new_token", NewToken diff --git a/elixir/apps/web/test/web/live/settings/api_clients/beta_test.exs b/elixir/apps/web/test/web/live/settings/api_clients/beta_test.exs new file mode 100644 index 000000000..8f46677a3 --- /dev/null +++ b/elixir/apps/web/test/web/live/settings/api_clients/beta_test.exs @@ -0,0 +1,98 @@ +defmodule Web.Live.Settings.ApiClients.BetaTest do + use Web.ConnCase, async: true + + setup do + account = Fixtures.Accounts.create_account() + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: [type: :account_admin_user]) + + %{ + account: account, + actor: actor, + identity: identity + } + end + + test "redirects to sign in page for unauthorized user", %{account: account, conn: conn} do + path = ~p"/#{account}/settings/api_clients/beta" + + assert live(conn, path) == + {:error, + {:redirect, + %{ + to: ~p"/#{account}?#{%{redirect_to: path}}", + flash: %{"error" => "You must sign in to access this page."} + }}} + end + + test "redirects to API client index when feature enabled", %{ + account: account, + identity: identity, + conn: conn + } do + assert {:error, {:live_redirect, %{to: path, flash: _}}} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/settings/api_clients/beta") + + assert path == ~p"/#{account}/settings/api_clients" + end + + test "renders breadcrumbs item", %{ + account: account, + identity: identity, + conn: conn + } do + features = Map.from_struct(account.features) + attrs = %{features: %{features | rest_api: false}} + + {:ok, account} = Domain.Accounts.update_account(account, attrs) + + {:ok, _lv, html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/settings/api_clients/beta") + + assert item = Floki.find(html, "[aria-label='Breadcrumb']") + breadcrumbs = String.trim(Floki.text(item)) + assert breadcrumbs =~ "API Clients" + assert breadcrumbs =~ "Beta" + end + + test "sends beta request email", %{ + account: account, + identity: identity, + conn: conn + } do + attrs = %{ + features: %{ + rest_api: false, + traffic_filters: true, + flow_activities: true, + policy_conditions: true, + multi_site_resources: true, + idp_sync: true + } + } + + {:ok, account} = Domain.Accounts.update_account(account, attrs) + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/settings/api_clients/beta") + + assert lv + |> element("#beta-request") + |> render_click() + |> Floki.find(".flash-info") + |> element_to_text() =~ "request to join" + + assert_email_sent(fn email -> + assert email.subject == "REST API Beta Request - #{account.slug}" + assert email.text_body =~ "REST API Beta Request" + assert email.text_body =~ "#{account.id}" + assert email.text_body =~ "#{account.slug}" + end) + end +end diff --git a/elixir/apps/web/test/web/live/settings/api_clients/index_test.exs b/elixir/apps/web/test/web/live/settings/api_clients/index_test.exs index 16f90fe5e..1a274b19c 100644 --- a/elixir/apps/web/test/web/live/settings/api_clients/index_test.exs +++ b/elixir/apps/web/test/web/live/settings/api_clients/index_test.exs @@ -27,19 +27,22 @@ defmodule Web.Live.Settings.ApiClients.IndexTest do }}} end - test "does not display API clients link when feature disabled", %{ + test "redirects to beta page when feature not enabled for account", %{ account: account, identity: identity, conn: conn } do - Domain.Config.feature_flag_override(:rest_api, false) + features = Map.from_struct(account.features) + attrs = %{features: %{features | rest_api: false}} - {:ok, _lv, html} = - conn - |> authorize_conn(identity) - |> live(~p"/#{account}/sites") + {:ok, account} = Domain.Accounts.update_account(account, attrs) - assert Floki.find(html, "a[href=\"/#{account.slug}/settings/api_clients\"]") == [] + assert {:error, {:live_redirect, %{to: path, flash: _}}} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/settings/api_clients") + + assert path == ~p"/#{account}/settings/api_clients/beta" end test "renders breadcrumbs item", %{ diff --git a/elixir/apps/web/test/web/live/settings/api_clients/new_test.exs b/elixir/apps/web/test/web/live/settings/api_clients/new_test.exs index af6930807..573530bd8 100644 --- a/elixir/apps/web/test/web/live/settings/api_clients/new_test.exs +++ b/elixir/apps/web/test/web/live/settings/api_clients/new_test.exs @@ -28,6 +28,24 @@ defmodule Web.Live.Settings.ApiClient.NewTest do }}} end + test "redirects to beta page when feature not enabled for account", %{ + account: account, + identity: identity, + conn: conn + } do + features = Map.from_struct(account.features) + attrs = %{features: %{features | rest_api: false}} + + {:ok, account} = Domain.Accounts.update_account(account, attrs) + + assert {:error, {:live_redirect, %{to: path, flash: _}}} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/settings/api_clients/new") + + assert path == ~p"/#{account}/settings/api_clients/beta" + end + test "renders breadcrumbs item", %{ account: account, identity: identity,