feat(portal): Add REST API closed beta page (#6027)

Why:

* Before the REST API is release to all Firezone users a closed beta
program will be run. Rather than blurring out the API Clients page for
users that are not apart of the closed beta program, a 'beta' page will
be shown that will allow users to request access to the closed beta.
Once the REST API is released to all accounts, all of this can be
removed.

Closes: #5920 

### Screenshot
<img width="1445" alt="Screenshot 2024-07-24 at 6 55 36 PM"
src="https://github.com/user-attachments/assets/a09591bc-190c-4bd4-9716-9a74a0f09e0a">
This commit is contained in:
Brian Manifold
2024-07-29 18:06:59 -04:00
committed by GitHub
parent 09916dea7e
commit edc80129c8
13 changed files with 322 additions and 72 deletions

View File

@@ -72,10 +72,7 @@
Identity Providers
</:item>
<:item navigate={~p"/#{@account}/settings/dns"}>DNS</:item>
<:item
:if={Domain.Accounts.rest_api_enabled?(@account)}
navigate={~p"/#{@account}/settings/api_clients"}
>
<:item navigate={~p"/#{@account}/settings/api_clients"}>
API Clients
</:item>
</.sidebar_item_group>

View File

@@ -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>
<.breadcrumb path={~p"/#{@account}/settings/api_clients/beta"}>Beta</.breadcrumb>
</.breadcrumbs>
<.section>
<:title><%= @page_title %></:title>
<:help>
API Clients are used to manage Firezone configuration through a REST API. See our
<a class={link_style()} href="api.firezone.dev/swaggerui">interactive API docs</a>
</:help>
<:content>
<.flash kind={:info}>
<p class="flex items-center gap-1.5 text-sm font-semibold leading-6">
<span class="hero-wrench-screwdriver h-4 w-4"></span> REST API Beta
</p>
The REST API is currently in closed beta.
<span :if={@requested == false}>
<p>
<a
id="beta-request"
href="#"
class="text-accent-900 underline"
phx-click="request_access"
>
Click here
</a>
to request access.
</p>
</span>
<span :if={@requested == true}>
<p>
Your request to join the closed beta has been made.
</p>
</span>
</.flash>
</:content>
</.section>
"""
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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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 %>

View File

@@ -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

View File

@@ -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

View File

@@ -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", %{

View File

@@ -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,