mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
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:
@@ -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>
|
||||
|
||||
73
elixir/apps/web/lib/web/live/settings/api_clients/beta.ex
Normal file
73
elixir/apps/web/lib/web/live/settings/api_clients/beta.ex
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
39
elixir/apps/web/lib/web/mailer/beta_email.ex
Normal file
39
elixir/apps/web/lib/web/mailer/beta_email.ex
Normal 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
|
||||
@@ -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 %>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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", %{
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user