UX cleanup pt 3 (#2789)

Closes https://github.com/firezone/firezone/issues/2601
Also addresses a lot of TODOs from
https://github.com/firezone/firezone/issues/2788
<img width="1728" alt="Screenshot 2023-12-01 at 18 25 11"
src="https://github.com/firezone/firezone/assets/1877644/95137fca-15ab-4b8c-9598-16d92a7951c7">
<img width="1728" alt="Screenshot 2023-12-01 at 18 25 16"
src="https://github.com/firezone/firezone/assets/1877644/9315b754-c3de-4336-8b59-c1d87ac83f69">
<img width="1728" alt="Screenshot 2023-12-01 at 18 25 33"
src="https://github.com/firezone/firezone/assets/1877644/65245194-c922-401e-bbc4-ff4a378520d2">
<img width="1728" alt="Screenshot 2023-12-01 at 18 25 39"
src="https://github.com/firezone/firezone/assets/1877644/3ac8c2c8-c0a8-4074-9cb1-123bc2c21e71">
<img width="1728" alt="Screenshot 2023-12-01 at 18 25 59"
src="https://github.com/firezone/firezone/assets/1877644/7a96cf74-3a9a-4215-9b22-871dee335b30">
This commit is contained in:
Andrew Dryga
2023-12-04 12:56:31 -06:00
committed by GitHub
parent 9a5f4e0ce2
commit 4fb101ed9f
35 changed files with 589 additions and 122 deletions

View File

@@ -35,7 +35,8 @@ defmodule Domain.Auth do
true <- Validator.valid_uuid?(id) do
{preload, _opts} = Keyword.pop(opts, :preload, [])
Provider.Query.by_id(id)
Provider.Query.all()
|> Provider.Query.by_id(id)
|> Authorizer.for_subject(Provider, subject)
|> Repo.fetch()
|> case do

View File

@@ -33,7 +33,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do
def identity_changeset(%Provider{} = _provider, %Ecto.Changeset{} = changeset) do
changeset
|> Domain.Validator.trim_change(:provider_identifier)
|> Ecto.Changeset.put_change(:provider_state, %{})
|> Domain.Validator.copy_change(:provider_virtual_state, :provider_state)
|> Ecto.Changeset.put_change(:provider_virtual_state, %{})
end

View File

@@ -68,7 +68,7 @@ defmodule Domain.Auth.Provider.Changeset do
def enable_provider(%Provider{} = provider) do
provider
|> change()
|> put_default_value(:disabled_at, nil)
|> put_change(:disabled_at, nil)
end
def delete_provider(%Provider{} = provider) do

View File

@@ -70,6 +70,22 @@ defmodule Domain.Policies do
end
end
def disable_policy(%Policy{} = policy, %Auth.Subject{} = subject) do
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_policies_permission()) do
Policy.Query.by_id(policy.id)
|> Authorizer.for_subject(subject)
|> Repo.fetch_and_update(with: &Policy.Changeset.disable(&1, subject))
end
end
def enable_policy(%Policy{} = policy, %Auth.Subject{} = subject) do
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_policies_permission()) do
Policy.Query.by_id(policy.id)
|> Authorizer.for_subject(subject)
|> Repo.fetch_and_update(with: &Policy.Changeset.enable/1)
end
end
def delete_policy(%Policy{} = policy, %Auth.Subject{} = subject) do
required_permissions =
{:one_of, [Authorizer.manage_policies_permission()]}

View File

@@ -11,6 +11,7 @@ defmodule Domain.Policies.Policy do
field :created_by, Ecto.Enum, values: ~w[identity]a
belongs_to :created_by_identity, Domain.Auth.Identity
field :disabled_at, :utc_datetime_usec
field :deleted_at, :utc_datetime_usec
timestamps()
end

View File

@@ -24,6 +24,18 @@ defmodule Domain.Policies.Policy.Changeset do
|> changeset()
end
def disable(%Policy{} = policy, %Auth.Subject{}) do
policy
|> change()
|> put_default_value(:disabled_at, DateTime.utc_now())
end
def enable(%Policy{} = policy) do
policy
|> change()
|> put_change(:disabled_at, nil)
end
def delete(%Policy{} = policy) do
policy
|> change()

View File

@@ -0,0 +1,9 @@
defmodule Domain.Repo.Migrations.AddPoliciesDisabledFields do
use Ecto.Migration
def change do
alter table(:policies) do
add(:disabled_at, :utc_datetime_usec)
end
end
end

View File

@@ -22,8 +22,17 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do
end
test "puts default provider state", %{provider: provider, changeset: changeset} do
changeset =
Ecto.Changeset.put_change(changeset, :provider_virtual_state, %{
"userinfo" => %{"email" => "foo@example.com"}
})
assert %Ecto.Changeset{} = changeset = identity_changeset(provider, changeset)
assert changeset.changes == %{provider_virtual_state: %{}}
assert changeset.changes == %{
provider_virtual_state: %{},
provider_state: %{"userinfo" => %{"email" => "foo@example.com"}}
}
end
test "trims provider identifier", %{provider: provider, changeset: changeset} do

View File

@@ -22,8 +22,17 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
end
test "puts default provider state", %{provider: provider, changeset: changeset} do
changeset =
Ecto.Changeset.put_change(changeset, :provider_virtual_state, %{
"userinfo" => %{"email" => "foo@example.com"}
})
assert %Ecto.Changeset{} = changeset = identity_changeset(provider, changeset)
assert changeset.changes == %{provider_state: %{}, provider_virtual_state: %{}}
assert changeset.changes == %{
provider_virtual_state: %{},
provider_state: %{"userinfo" => %{"email" => "foo@example.com"}}
}
end
test "trims provider identifier", %{provider: provider, changeset: changeset} do

View File

@@ -75,11 +75,12 @@ defmodule Domain.AuthTest do
assert fetch_provider_by_id("foo", subject) == {:error, :not_found}
end
test "returns error when provider is deleted", %{account: account, subject: subject} do
test "returns deleted provider", %{account: account, subject: subject} do
provider = Fixtures.Auth.create_userpass_provider(account: account)
{:ok, _provider} = delete_provider(provider, subject)
assert fetch_provider_by_id(provider.id, subject) == {:error, :not_found}
assert {:ok, fetched_provider} = fetch_provider_by_id(provider.id, subject)
assert fetched_provider.id == provider.id
end
test "returns provider", %{account: account, subject: subject} do
@@ -877,11 +878,12 @@ defmodule Domain.AuthTest do
subject: subject,
provider: provider
} do
assert {:ok, provider} = enable_provider(provider, subject)
assert provider.disabled_at
assert {:ok, provider} = enable_provider(provider, subject)
assert is_nil(provider.disabled_at)
assert provider = Repo.get(Auth.Provider, provider.id)
assert provider.disabled_at
assert is_nil(provider.disabled_at)
end
test "does not do anything when an provider is enabled twice", %{

View File

@@ -341,6 +341,117 @@ defmodule Domain.PoliciesTest do
end
end
describe "disable_policy/2" do
setup context do
policy =
Fixtures.Policies.create_policy(
account: context.account,
subject: context.subject
)
Map.put(context, :policy, policy)
end
test "disables a given policy", %{
account: account,
subject: subject,
policy: policy
} do
other_policy = Fixtures.Policies.create_policy(account: account)
assert {:ok, policy} = disable_policy(policy, subject)
assert policy.disabled_at
assert policy = Repo.get(Policies.Policy, policy.id)
assert policy.disabled_at
assert other_policy = Repo.get(Policies.Policy, other_policy.id)
assert is_nil(other_policy.disabled_at)
end
test "does not do anything when an policy is disabled twice", %{
subject: subject,
account: account
} do
policy = Fixtures.Policies.create_policy(account: account)
assert {:ok, _policy} = disable_policy(policy, subject)
assert {:ok, policy} = disable_policy(policy, subject)
assert {:ok, _policy} = disable_policy(policy, subject)
end
test "does not allow to disable policies in other accounts", %{
subject: subject
} do
policy = Fixtures.Policies.create_policy()
assert disable_policy(policy, subject) == {:error, :not_found}
end
test "returns error when subject can not disable policies", %{
subject: subject,
policy: policy
} do
subject = Fixtures.Auth.remove_permissions(subject)
assert disable_policy(policy, subject) ==
{:error,
{:unauthorized,
[missing_permissions: [Policies.Authorizer.manage_policies_permission()]]}}
end
end
describe "enable_policy/2" do
setup context do
policy =
Fixtures.Policies.create_policy(
account: context.account,
subject: context.subject
)
{:ok, policy} = disable_policy(policy, context.subject)
Map.put(context, :policy, policy)
end
test "enables a given policy", %{
subject: subject,
policy: policy
} do
assert {:ok, policy} = enable_policy(policy, subject)
assert is_nil(policy.disabled_at)
assert policy = Repo.get(Policies.Policy, policy.id)
assert is_nil(policy.disabled_at)
end
test "does not do anything when an policy is enabled twice", %{
subject: subject,
policy: policy
} do
assert {:ok, _policy} = enable_policy(policy, subject)
assert {:ok, policy} = enable_policy(policy, subject)
assert {:ok, _policy} = enable_policy(policy, subject)
end
test "does not allow to enable policies in other accounts", %{
subject: subject
} do
policy = Fixtures.Policies.create_policy()
assert enable_policy(policy, subject) == {:error, :not_found}
end
test "returns error when subject can not enable policies", %{
subject: subject,
policy: policy
} do
subject = Fixtures.Auth.remove_permissions(subject)
assert enable_policy(policy, subject) ==
{:error,
{:unauthorized,
[missing_permissions: [Policies.Authorizer.manage_policies_permission()]]}}
end
end
describe "delete_policy/2" do
setup context do
policy =

View File

@@ -15,6 +15,18 @@ Hooks.Tabs = {
}
}
Hooks.Refocus = {
mounted() {
this.el.addEventListener("click", (ev) => {
ev.preventDefault();
let target_id = ev.currentTarget.getAttribute("data-refocus");
let el = document.getElementById(target_id);
if (document.activeElement === el) return;
el.focus();
});
}
}
Hooks.Copy = {
mounted() {
this.el.addEventListener("click", (ev) => {
@@ -31,14 +43,14 @@ Hooks.Copy = {
icon_cl.add("hero-clipboard-document-check");
icon_cl.add("text-green-500");
icon_cl.remove("hero-clipboard-document");
content.innerHTML = "Copied"
if (content) { content.innerHTML = "Copied" }
});
setTimeout(() => {
icon_cl.remove("hero-clipboard-document-check");
icon_cl.remove("text-green-500");
icon_cl.add("hero-clipboard-document");
content.innerHTML = "Copy"
if (content) { content.innerHTML = "Copy" }
}, 2000);
});
},

View File

@@ -1,6 +1,6 @@
{
"dependencies": {
"@fontsource/source-sans-pro": "^5.0.8",
"flowbite": "^2.1.1"
"flowbite": "^2.2.0"
}
}

View File

@@ -9,8 +9,8 @@ dependencies:
specifier: ^5.0.8
version: 5.0.8
flowbite:
specifier: ^2.1.1
version: 2.1.1
specifier: ^2.2.0
version: 2.2.0
packages:
@@ -22,8 +22,8 @@ packages:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
/flowbite@2.1.1:
resolution: {integrity: sha512-FkTwNXlfWRXUhSsiE9D4bEqLN8ywdunW2qOz1z7gN+Co7h9EQIJf/Fr4xgq1NJPFa+6MeMf08QLzviU7LqB/rQ==}
/flowbite@2.2.0:
resolution: {integrity: sha512-Eq0qWz4a5nlxaGuUcspzpu+8Ny0A7lKEJEKcuPpkdSoF8tWjbKeuVVgKk8/q10ZE9bhXh1GHBXdCsRxu0LZTNQ==}
dependencies:
'@popperjs/core': 2.11.8
mini-svg-data-uri: 1.4.4

View File

@@ -70,6 +70,8 @@ defmodule Web.Auth do
client_auth_token: client_token,
client_csrf_token: client_csrf_token,
actor_name: subject.actor.name,
account_slug: subject.account.slug,
account_name: subject.account.name,
identity_provider_identifier: subject.identity.provider_identifier
}
|> Enum.reject(&is_nil(elem(&1, 1)))

View File

@@ -90,6 +90,31 @@ defmodule Web.CoreComponents do
"""
end
@doc """
Render an inlined copy-paste button to the right of the content block.
## Examples
<.copy id="foo">
The lazy brown fox jumped over the quick dog.
</.copy>
"""
attr :id, :string, required: true
attr :class, :string, default: ""
slot :inner_block, required: true
attr :rest, :global
def copy(assigns) do
~H"""
<div id={@id} phx-hook="Copy" class={@class} {@rest}>
<code data-copy phx-no-format><%= render_slot(@inner_block) %></code>
<span class={~w[text-gray-400 cursor-pointer rounded]}>
<.icon name="hero-clipboard-document" data-icon class="h-4 w-4" />
</span>
</div>
"""
end
@doc """
Render a tabs toggle container and its content.

View File

@@ -23,6 +23,7 @@ defmodule Web.FormComponents do
attr :id, :any, default: nil
attr :name, :any
attr :label, :string, default: nil
attr :prefix, :string, default: nil
attr :value, :any
attr :value_id, :any,
@@ -204,7 +205,51 @@ defmodule Web.FormComponents do
"""
end
# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(%{type: "text", prefix: prefix} = assigns) when not is_nil(prefix) do
~H"""
<div phx-feedback-for={@name}>
<.label :if={not is_nil(@label)} for={@id}><%= @label %></.label>
<div class={[
"flex items-center",
"text-sm text-gray-900 bg-gray-50",
"border-gray-300 border rounded",
"w-full",
"phx-no-feedback:border-gray-300",
"focus-within:outline-none focus-within:border-blue-600",
"peer-disabled:bg-slate-50 peer-disabled:text-slate-500 peer-disabled:border-slate-200 peer-disabled:shadow-none",
@errors != [] && "border-rose-400"
]}>
<div
class={[
"-mr-5",
"select-none cursor-text",
"text-gray-500",
"p-2.5 block"
]}
id={"#{@id}-prefix"}
phx-hook="Refocus"
data-refocus={@id}
>
<%= @prefix %>
</div>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"text-sm text-gray-900 bg-transparent border-0",
"p-2.5 block w-full",
"focus:outline-none focus:border-0 focus:ring-0"
]}
{@rest}
/>
</div>
<.error :for={msg <- @errors} data-validation-error-for={@name}><%= msg %></.error>
</div>
"""
end
def input(assigns) do
~H"""
<div phx-feedback-for={@name}>

View File

@@ -38,7 +38,7 @@ defmodule Web.PageComponents do
slot :action, required: false, doc: "A slot for action to the right of the title"
slot :content, required: true, doc: "A slot for content of the section" do
slot :content, required: false, doc: "A slot for content of the section" do
attr :flash, :any, doc: "The flash to be displayed above the content"
end

View File

@@ -2,7 +2,7 @@ defmodule Web.HomeController do
use Web, :controller
alias Domain.Accounts
def home(conn, _params) do
def home(conn, params) do
{accounts, conn} =
with {:ok, recent_account_ids, conn} <- Web.Auth.list_recent_account_ids(conn),
{:ok, accounts} <- Accounts.list_accounts_by_ids(recent_account_ids) do
@@ -16,12 +16,22 @@ defmodule Web.HomeController do
_other -> {[], conn}
end
redirect_params =
take_non_empty_params(params, ["client_platform", "client_csrf_token"])
conn
|> put_layout(html: {Web.Layouts, :public})
|> render("home.html", accounts: accounts)
|> render("home.html", accounts: accounts, redirect_params: redirect_params)
end
def redirect_to_sign_in(conn, %{"account_id_or_slug" => account_id_or_slug}) do
redirect(conn, to: ~p"/#{account_id_or_slug}")
def redirect_to_sign_in(conn, %{"account_id_or_slug" => account_id_or_slug} = params) do
redirect_params =
take_non_empty_params(params, ["client_platform", "client_csrf_token"])
redirect(conn, to: ~p"/#{account_id_or_slug}?#{redirect_params}")
end
defp take_non_empty_params(map, keys) do
map |> Map.take(keys) |> Map.reject(fn {_key, value} -> value in ["", nil] end)
end
end

View File

@@ -21,25 +21,39 @@ defmodule Web.HomeHTML do
</h3>
<div :if={@accounts != []} class="space-y-3 items-center">
<.account_button :for={account <- @accounts} account={account} />
<.account_button
:for={account <- @accounts}
account={account}
redirect_params={@redirect_params}
/>
</div>
<.separator :if={@accounts != []} />
<.form :let={f} for={%{}} action={~p"/"} class="space-y-4 lg:space-y-6">
<.form
:let={f}
for={%{}}
action={~p"/?#{@redirect_params}"}
class="space-y-4 lg:space-y-6"
>
<.input
field={f[:account_id_or_slug]}
type="text"
label="Account ID or Slug"
placeholder={~s|As shown in your "Welcome to Firezone" email|}
prefix={url(~p"/")}
required
autofocus
/>
<p>As shown in your "Welcome to Firezone" email</p>
<.button class="w-full">
Go to Sign In page
</.button>
</.form>
<p :if={Domain.Config.sign_up_enabled?()} class="py-2">
<p
:if={Domain.Config.sign_up_enabled?() and is_nil(@redirect_params["client_platform"])}
class="py-2"
>
Don't have an account?
<a href={~p"/sign_up"} class="font-medium text-blue-600 hover:text-blue-500">
Sign up here.
@@ -54,7 +68,7 @@ defmodule Web.HomeHTML do
def account_button(assigns) do
~H"""
<a href={~p"/#{@account}"} class={~w[
<a href={~p"/#{@account}?#{@redirect_params}"} class={~w[
w-full inline-flex items-center justify-center py-2.5 px-5
bg-white rounded
text-sm font-medium text-gray-900

View File

@@ -12,6 +12,8 @@ defmodule Web.Clients.Index do
end
end
# subscribe for presence
def render(assigns) do
~H"""
<.breadcrumbs account={@account}>

View File

@@ -42,6 +42,17 @@ defmodule Web.Policies.Index do
<%= policy.resource.name %>
</.link>
</:col>
<:col :let={policy} label="STATUS">
<%= if is_nil(policy.deleted_at) do %>
<%= if is_nil(policy.disabled_at) do %>
Active
<% else %>
Disabled
<% end %>
<% else %>
Deleted
<% end %>
</:col>
<:empty>
<div class="flex justify-center text-center text-slate-500 p-4">
<div class="w-auto">

View File

@@ -38,6 +38,7 @@ defmodule Web.Policies.Show do
<.section>
<:title>
<%= @page_title %>: <code><%= @policy.id %></code>
<span :if={not is_nil(@policy.disabled_at)} class="text-orange-600">(disabled)</span>
<span :if={not is_nil(@policy.deleted_at)} class="text-red-600">(deleted)</span>
</:title>
<:action :if={is_nil(@policy.deleted_at)}>
@@ -45,6 +46,22 @@ defmodule Web.Policies.Show do
Edit Policy
</.edit_button>
</:action>
<:action :if={is_nil(@policy.deleted_at)}>
<.button
:if={not is_nil(@policy.disabled_at)}
phx-click="enable"
data-confirm="Are you sure want to enable this policy?"
>
Enable Policy
</.button>
<.button
:if={is_nil(@policy.disabled_at)}
phx-click="disable"
data-confirm="Are you sure want to disable this policy? All authorizations will be revoked and users can loose access to the resource."
>
Disable Policy
</.button>
</:action>
<:content>
<.vertical_table id="policy">
<.vertical_table_row>
@@ -157,12 +174,37 @@ defmodule Web.Policies.Show do
Delete Policy
</.delete_button>
</:action>
<:content></:content>
</.danger_zone>
"""
end
def handle_event("delete", %{"id" => _policy_id}, socket) do
def handle_event("disable", _params, socket) do
{:ok, policy} = Policies.disable_policy(socket.assigns.policy, socket.assigns.subject)
policy = %{
policy
| actor_group: socket.assigns.policy.actor_group,
resource: socket.assigns.policy.resource,
created_by_identity: socket.assigns.policy.created_by_identity
}
{:noreply, assign(socket, policy: policy)}
end
def handle_event("enable", _params, socket) do
{:ok, policy} = Policies.enable_policy(socket.assigns.policy, socket.assigns.subject)
policy = %{
policy
| actor_group: socket.assigns.policy.actor_group,
resource: socket.assigns.policy.resource,
created_by_identity: socket.assigns.policy.created_by_identity
}
{:noreply, assign(socket, policy: policy)}
end
def handle_event("delete", _params, socket) do
{:ok, _} = Policies.delete_policy(socket.assigns.policy, socket.assigns.subject)
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/policies")}
end

View File

@@ -24,7 +24,9 @@ defmodule Web.Settings.Account do
</.vertical_table_row>
<.vertical_table_row>
<:label>Account Slug</:label>
<:value><%= @account.slug %></:value>
<:value>
<.copy id="account-slug"><%= @account.slug %></.copy>
</:value>
</.vertical_table_row>
</.vertical_table>
</div>

View File

@@ -29,29 +29,35 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Show do
<.section>
<:title>
Identity Provider <code><%= @provider.name %></code>
<span :if={not is_nil(@provider.disabled_at)} class="text-orange-600">(disabled)</span>
<span :if={not is_nil(@provider.deleted_at)} class="text-red-600">(deleted)</span>
</:title>
<:action>
<:action :if={is_nil(@provider.deleted_at)}>
<.edit_button navigate={
~p"/#{@account}/settings/identity_providers/google_workspace/#{@provider.id}/edit"
}>
Edit
</.edit_button>
</:action>
<:action>
<:action :if={is_nil(@provider.deleted_at)}>
<%= if @provider.adapter_state["status"] != "pending_access_token" do %>
<.button :if={not is_nil(@provider.disabled_at)} phx-click="enable">
<.button
:if={not is_nil(@provider.disabled_at)}
phx-click="enable"
data-confirm="Are you sure want to enable this provider?"
>
Enable Identity Provider
</.button>
<.button
:if={is_nil(@provider.disabled_at)}
phx-click="disable"
data-confirm="Are you sure want to disable this provider?"
data-confirm="Are you sure want to disable this provider? All authorizations will be revoked and actors won't be able to use it to access Firezone."
>
Disable Identity Provider
</.button>
<% end %>
</:action>
<:action>
<:action :if={is_nil(@provider.deleted_at)}>
<.button
style="primary"
navigate={
@@ -96,7 +102,7 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Show do
</:content>
</.section>
<.danger_zone>
<.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?"

View File

@@ -28,8 +28,10 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Show do
<.section>
<:title>
Identity Provider <code><%= @provider.name %></code>
<span :if={not is_nil(@provider.disabled_at)} class="text-orange-600">(disabled)</span>
<span :if={not is_nil(@provider.deleted_at)} class="text-red-600">(deleted)</span>
</:title>
<:action>
<:action :if={is_nil(@provider.deleted_at)}>
<.edit_button navigate={
~p"/#{@account}/settings/identity_providers/openid_connect/#{@provider}/edit"
}>
@@ -37,21 +39,25 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Show do
</.edit_button>
</:action>
<!-- Wondering if these next two buttons can be combined? -->
<:action>
<.button :if={not is_nil(@provider.disabled_at)} phx-click="enable">
<:action :if={is_nil(@provider.deleted_at)}>
<.button
:if={not is_nil(@provider.disabled_at)}
phx-click="enable"
data-confirm="Are you sure want to enable this provider?"
>
Enable
</.button>
</:action>
<:action>
<:action :if={is_nil(@provider.deleted_at)}>
<.button
:if={is_nil(@provider.disabled_at)}
phx-click="disable"
data-confirm="Are you sure want to disable this provider?"
data-confirm="Are you sure want to disable this provider? All authorizations will be revoked and actors won't be able to use it to access Firezone."
>
Disable
</.button>
</:action>
<:action>
<:action :if={is_nil(@provider.deleted_at)}>
<.button
style="primary"
navigate={
@@ -115,10 +121,7 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Show do
</div>
</:content>
</.section>
<.section>
<:title>
Danger zone
</:title>
<.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?"
@@ -127,8 +130,7 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Show do
Delete Identity Provider
</.delete_button>
</:action>
<:content></:content>
</.section>
</.danger_zone>
"""
end

View File

@@ -31,15 +31,21 @@ defmodule Web.Settings.IdentityProviders.System.Show do
<.section>
<:title>
Identity Provider <code><%= @provider.name %></code>
<span :if={not is_nil(@provider.disabled_at)} class="text-orange-600">(disabled)</span>
<span :if={not is_nil(@provider.deleted_at)} class="text-red-600">(deleted)</span>
</:title>
<:action>
<.button :if={not is_nil(@provider.disabled_at)} phx-click="enable">
<:action :if={is_nil(@provider.deleted_at)}>
<.button
:if={not is_nil(@provider.disabled_at)}
phx-click="enable"
data-confirm="Are you sure want to enable this provider?"
>
Enable Identity Provider
</.button>
<.button
:if={is_nil(@provider.disabled_at)}
phx-click="disable"
data-confirm="Are you sure want to disable this provider?"
data-confirm="Are you sure want to disable this provider? All authorizations will be revoked and actors won't be able to use it to access Firezone."
>
Disable Identity Provider
</.button>
@@ -74,10 +80,7 @@ defmodule Web.Settings.IdentityProviders.System.Show do
</:content>
</.section>
<.section>
<:title>
Danger zone
</:title>
<.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?"
@@ -86,8 +89,7 @@ defmodule Web.Settings.IdentityProviders.System.Show do
Delete Identity Provider
</.delete_button>
</:action>
<:content></:content>
</.section>
</.danger_zone>
"""
end

View File

@@ -16,8 +16,8 @@ defmodule Web.SignUp do
embeds_one(:actor, Actors.Actor)
end
def changeset(%Registration{} = registration, attrs) do
registration
def changeset(attrs) do
%Registration{}
|> Ecto.Changeset.cast(attrs, [:email])
|> Ecto.Changeset.validate_required([:email])
|> Ecto.Changeset.validate_format(:email, ~r/.+@.+/)
@@ -35,7 +35,7 @@ defmodule Web.SignUp do
real_ip = Web.Auth.real_ip(socket)
changeset =
Registration.changeset(%Registration{}, %{
Registration.changeset(%{
account: %{slug: "placeholder"},
actor: %{type: :account_admin_user}
})
@@ -44,9 +44,12 @@ defmodule Web.SignUp do
assign(socket,
form: to_form(changeset),
account: nil,
provider: nil,
user_agent: user_agent,
real_ip: real_ip,
sign_up_enabled?: Config.sign_up_enabled?()
sign_up_enabled?: Config.sign_up_enabled?(),
account_name_changed?: false,
actor_name_changed?: false
)
{:ok, socket}
@@ -76,7 +79,12 @@ defmodule Web.SignUp do
<:item>
<.sign_up_form :if={@account == nil && @sign_up_enabled?} flash={@flash} form={@form} />
<.welcome :if={@account && @sign_up_enabled?} account={@account} />
<.welcome
:if={@account && @sign_up_enabled?}
account={@account}
provider={@provider}
identity={@identity}
/>
<.sign_up_disabled :if={!@sign_up_enabled?} />
</:item>
</.intersperse_blocks>
@@ -101,41 +109,61 @@ defmodule Web.SignUp do
~H"""
<div class="space-y-6">
<div class="text-center text-gray-900">
Your account has been created! Please check your email for sign in instructions.
Your account has been created!
<p>Please check your email for sign in instructions.</p>
</div>
<div class="text-center">
<div class="px-12">
<table class="border-collapse table-fixed w-full text-sm">
<table class="border-collapse w-full text-sm">
<tbody>
<tr>
<td class={~w[border-b border-slate-100 p-4 pl-8 text-gray-900]}>
<td class={~w[border-b border-slate-100 py-4 text-gray-900]}>
Account Name:
</td>
<td class={~w[border-b border-slate-100 p-4 pl-8 text-gray-900]}>
<td class={~w[border-b border-slate-100 py-4 text-gray-900]}>
<%= @account.name %>
</td>
</tr>
<tr>
<td class={~w[border-b border-slate-100 p-4 pl-8 text-gray-900]}>
<td class={~w[border-b border-slate-100 py-4 text-gray-900]}>
Account Slug:
</td>
<td class={~w[border-b border-slate-100 p-4 pl-8 text-gray-900]}>
<td class={~w[border-b border-slate-100 py-4 text-gray-900]}>
<%= @account.slug %>
</td>
</tr>
<tr>
<td class={~w[border-b border-slate-100 py-4 text-gray-900]}>
Sign In URL:
</td>
<td class={~w[border-b border-slate-100 py-4 text-gray-900]}>
<.link class="font-medium text-blue-600 hover:underline" navigate={~p"/#{@account}"}>
<%= url(~p"/#{@account}") %>
</.link>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="text-base leading-7 text-center text-gray-900">
<div>
Sign In URL
</div>
<div>
<.link class="font-medium text-blue-600 hover:underline" navigate={~p"/#{@account.slug}"}>
<%= "#{Web.Endpoint.url()}/#{@account.slug}" %>
</.link>
</div>
<.form
for={%{}}
id="resend-email"
as={:email}
class="inline"
action={~p"/#{@account}/sign_in/providers/#{@provider}/request_magic_link"}
method="post"
>
<.input
type="hidden"
name="email[provider_identifier]"
value={@identity.provider_identifier}
/>
<.submit_button class="w-full">
Sign In
</.submit_button>
</.form>
</div>
</div>
"""
@@ -147,12 +175,22 @@ defmodule Web.SignUp do
Sign Up Now
</h3>
<.simple_form for={@form} class="space-y-4 lg:space-y-6" phx-submit="submit" phx-change="validate">
<.input
field={@form[:email]}
type="text"
label="Email"
placeholder="Enter your work email here"
required
autofocus
phx-debounce="300"
/>
<.inputs_for :let={account} field={@form[:account]}>
<.input
field={account[:name]}
type="text"
label="Account Name"
placeholder="Enter an Account Name here"
placeholder="Enter an account name"
required
phx-debounce="300"
/>
@@ -171,15 +209,6 @@ defmodule Web.SignUp do
<.input field={actor[:type]} type="hidden" />
</.inputs_for>
<.input
field={@form[:email]}
type="text"
label="Email"
placeholder="Enter your email here"
required
phx-debounce="300"
/>
<:actions>
<.button phx-disable-with="Creating Account..." class="w-full">
Create Account
@@ -218,23 +247,37 @@ defmodule Web.SignUp do
"""
end
def handle_event("validate", %{"registration" => attrs}, socket) do
def handle_event("validate", %{"registration" => attrs} = payload, socket) do
account_name_changed? =
socket.assigns.account_name_changed? ||
payload["_target"] == ["registration", "account", "name"]
actor_name_changed? =
socket.assigns.actor_name_changed? ||
payload["_target"] == ["registration", "actor", "name"]
changeset =
%Registration{}
|> Registration.changeset(attrs)
attrs
|> maybe_put_default_account_name(account_name_changed?)
|> maybe_put_default_actor_name(actor_name_changed?)
|> Registration.changeset()
|> Map.put(:action, :validate)
socket = assign(socket, form: to_form(changeset))
{:noreply, socket}
{:noreply,
assign(socket,
form: to_form(changeset),
account_name_changed?: account_name_changed?,
actor_name_changed?: actor_name_changed?
)}
end
def handle_event("submit", %{"registration" => orig_attrs}, socket) do
attrs = put_in(orig_attrs, ["actor", "type"], :account_admin_user)
def handle_event("submit", %{"registration" => attrs}, socket) do
changeset =
%Registration{}
|> Registration.changeset(attrs)
attrs
|> maybe_put_default_account_name()
|> maybe_put_default_actor_name()
|> put_in(["actor", "type"], :account_admin_user)
|> Registration.changeset()
|> Map.put(:action, :insert)
if changeset.valid? && socket.assigns.sign_up_enabled? do
@@ -279,7 +322,7 @@ defmodule Web.SignUp do
)
case Domain.Repo.transaction(multi) do
{:ok, %{account: account, identity: identity}} ->
{:ok, %{account: account, provider: provider, identity: identity}} ->
{:ok, _} =
Web.Mailer.AuthEmail.sign_up_link_email(
account,
@@ -289,7 +332,7 @@ defmodule Web.SignUp do
)
|> Web.Mailer.deliver()
socket = assign(socket, account: account)
socket = assign(socket, account: account, provider: provider, identity: identity)
{:noreply, socket}
{:error, :account, err_changeset, _effects_so_far} ->
@@ -302,4 +345,31 @@ defmodule Web.SignUp do
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp maybe_put_default_account_name(attrs, account_name_changed? \\ true)
defp maybe_put_default_account_name(attrs, true) do
attrs
end
defp maybe_put_default_account_name(attrs, false) do
case String.split(attrs["email"], "@", parts: 2) do
[default_name | _] when byte_size(default_name) > 0 ->
put_in(attrs, ["account", "name"], "#{default_name}'s account")
_ ->
attrs
end
end
defp maybe_put_default_actor_name(attrs, actor_name_changed? \\ true)
defp maybe_put_default_actor_name(attrs, true) do
attrs
end
defp maybe_put_default_actor_name(attrs, false) do
[default_name | _] = String.split(attrs["email"], "@", parts: 2)
put_in(attrs, ["actor", "name"], default_name)
end
end

View File

@@ -80,7 +80,14 @@ defmodule Web.AuthTest do
redirected_to = conn |> signed_in_redirect(subject, "android", "foo") |> redirected_to()
assert redirected_to =~ "/handle_client_auth_callback?"
assert redirected_to =~ "client_auth_token="
assert redirected_to =~ "client_csrf_token=foo"
assert redirected_to =~ "actor_name=#{URI.encode_www_form(subject.actor.name)}"
assert redirected_to =~ "account_name=#{subject.account.name}"
assert redirected_to =~ "account_slug=#{subject.account.slug}"
assert redirected_to =~
"identity_provider_identifier=#{subject.identity.provider_identifier}"
end
test "redirects admin user to the post-login path if platform url is missing", %{

View File

@@ -183,5 +183,36 @@ defmodule Web.Live.Policies.ShowTest do
{:error, {:live_redirect, %{to: ~p"/#{account}/policies", kind: :push}}}
assert Repo.get(Domain.Policies.Policy, policy.id).deleted_at
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}")
assert html =~ "(deleted)"
end
test "allows disabling and enabling policy", %{
account: account,
policy: policy,
identity: identity,
conn: conn
} do
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}")
assert lv
|> element("button", "Disable Policy")
|> render_click() =~ "(disabled)"
assert Repo.get(Domain.Policies.Policy, policy.id).disabled_at
refute lv
|> element("button", "Enable Policy")
|> render_click() =~ "(disabled)"
refute Repo.get(Domain.Policies.Policy, policy.id).disabled_at
end
end

View File

@@ -56,6 +56,6 @@ defmodule Web.Live.Settings.Account.IndexTest do
assert rows["account name"] == account.name
assert rows["account id"] == account.id
assert rows["account slug"] == account.slug
assert rows["account slug"] =~ account.slug
end
end

View File

@@ -35,7 +35,7 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.ShowTest do
}}}
end
test "renders not found error when provider is deleted", %{
test "renders deleted provider without action buttons", %{
account: account,
provider: provider,
identity: identity,
@@ -43,11 +43,16 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.ShowTest do
} do
provider = Fixtures.Auth.delete_provider(provider)
assert_raise Web.LiveErrors.NotFoundError, fn ->
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/settings/identity_providers/google_workspace/#{provider}")
end
assert html =~ "(deleted)"
refute html =~ "Danger Zone"
refute html =~ "Add"
refute html =~ "Edit"
refute html =~ "Deploy"
end
test "renders breadcrumbs item", %{

View File

@@ -35,7 +35,7 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.ShowTest do
}}}
end
test "renders not found error when provider is deleted", %{
test "renders deleted provider without action buttons", %{
account: account,
provider: provider,
identity: identity,
@@ -43,11 +43,15 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.ShowTest do
} do
provider = Fixtures.Auth.delete_provider(provider)
assert_raise Web.LiveErrors.NotFoundError, fn ->
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/settings/identity_providers/openid_connect/#{provider}")
end
assert html =~ "(deleted)"
refute html =~ "Danger Zone"
refute html =~ "Edit"
refute html =~ "Deploy"
end
test "renders breadcrumbs item", %{

View File

@@ -31,7 +31,7 @@ defmodule Web.Live.Settings.IdentityProviders.System.ShowTest do
}}}
end
test "renders not found error when provider is deleted", %{
test "renders deleted provider without action buttons", %{
account: account,
provider: provider,
identity: identity,
@@ -39,11 +39,16 @@ defmodule Web.Live.Settings.IdentityProviders.System.ShowTest do
} do
provider = Fixtures.Auth.delete_provider(provider)
assert_raise Web.LiveErrors.NotFoundError, fn ->
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/settings/identity_providers/system/#{provider}")
end
assert html =~ "(deleted)"
refute html =~ "Danger Zone"
refute html =~ "Add"
refute html =~ "Edit"
refute html =~ "Deploy"
end
test "renders breadcrumbs item", %{

View File

@@ -6,8 +6,8 @@
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"chatterbox": {:hex, :ts_chatterbox, "0.13.0", "6f059d97bcaa758b8ea6fffe2b3b81362bd06b639d3ea2bb088335511d691ebf", [:rebar3], [{:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "b93d19104d86af0b3f2566c4cba2a57d2e06d103728246ba1ac6c3c0ff010aa7"},
"cldr_utils": {:hex, :cldr_utils, "2.24.1", "5ff8c8c55f96666228827bcf85a23d632022def200566346545d01d15e4c30dc", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "1820300531b5b849d0bc468e5a87cd64f8f2c5191916f548cbe69b2efc203780"},
"chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"},
"cldr_utils": {:hex, :cldr_utils, "2.24.2", "364fa30be55d328e704629568d431eb74cd2f085752b27f8025520b566352859", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "3362b838836a9f0fa309de09a7127e36e67310e797d556db92f71b548832c7cf"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
@@ -23,12 +23,12 @@
"ecto_sql": {:hex, :ecto_sql, "3.11.0", "c787b24b224942b69c9ff7ab9107f258ecdc68326be04815c6cce2941b6fad1c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "77aa3677169f55c2714dda7352d563002d180eb33c0dc29cd36d39c0a1a971f5"},
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"},
"ex_cldr": {:hex, :ex_cldr, "2.37.4", "3e2c04d9c691a75a8b7e808dfcbacedb9cdf3e73f819d1b4174f8f065e5f29c1", [:mix], [{:cldr_utils, "~> 2.21", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "2bcb5de0095324ba645b4e156bf346add156d8b928f0ffd985c61664d981ad3d"},
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
"ex_cldr": {:hex, :ex_cldr, "2.37.5", "9da6d97334035b961d2c2de167dc6af8cd3e09859301a5b8f49f90bd8b034593", [:mix], [{:cldr_utils, "~> 2.21", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "74ad5ddff791112ce4156382e171a5f5d3766af9d5c4675e0571f081fe136479"},
"ex_cldr_calendars": {:hex, :ex_cldr_calendars, "1.22.1", "3e5150f1fe7698e0fa118aeedcca1b5920d0a552bc40c81cf65ca9b0a4ea4cc3", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: true]}, {:ex_cldr_numbers, "~> 2.31", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.16", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e7408cd9e8318b2ef93b76728e84484ddc3ea6d7c894fbc811c54122a7140169"},
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.15.0", "aadd34e91cfac7ef6b03fe8f47f8c6fa8c5daf3f89b5d9fee64ec545ded839cf", [:mix], [{:ex_cldr, "~> 2.34", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "0521316396c66877a2d636219767560bb2397c583341fcb154ecf9f3000e6ff8"},
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.15.0", "63a3f611aec735bc2a29be8792db32d350277aadc07f0b1c09af539b87c93d66", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.22", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.31", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:tz, "~> 0.26", [hex: :tz, repo: "hexpm", optional: true]}], "hexpm", "4e61921901e8d58704f395f3e94b5ef6bcd02bc54aaf8475da3cf5297cb70369"},
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.32.2", "5e0e3031d3f54b51fe7078a7a94592987b70b06d631bdc88813b222dc5a8b1bd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.37", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, ">= 2.14.2", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "91257684a9c4d6abdf738f0cc5671837de876e69552e8bd4bc5fa1bfd5817713"},
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.15.1", "e92ba17c41e7405b7784e0e65f406b5f17cfe313e0e70de9befd653e12854822", [:mix], [{:ex_cldr, "~> 2.34", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "31df8bd37688340f8819bdd770eb17d659652078d34db632b85d4a32864d6a25"},
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.16.0", "d9848a5de83b6f1bcba151cc43d63b5c6311813cd605b1df1afd896dfdd21001", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.22", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.31", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:tz, "~> 0.26", [hex: :tz, repo: "hexpm", optional: true]}], "hexpm", "0f2f250d479cadda4e0ef3a5e3d936ae7ba1a3f1199db6791e284e86203495b1"},
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.32.3", "b631ff94c982ec518e46bf4736000a30a33d6b58facc085d5f240305f512ad4a", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.37", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, ">= 2.14.2", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "7b626ff1e59a0ec9c3c5db5ce9ca91a6995e2ab56426b71f3cbf67181ea225f5"},
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
"file_size": {:hex, :file_size, "3.0.1", "ad447a69442a82fc701765a73992d7b1110136fa0d4a9d3190ea685d60034dcd", [:mix], [{:decimal, ">= 1.0.0 and < 3.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:number, "~> 1.0", [hex: :number, repo: "hexpm", optional: false]}], "hexpm", "64dd665bc37920480c249785788265f5d42e98830d757c6a477b3246703b8e20"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
@@ -36,12 +36,12 @@
"floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
"gettext": {:hex, :gettext, "0.23.1", "821e619a240e6000db2fc16a574ef68b3bd7fe0167ccc264a81563cc93e67a31", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "19d744a36b809d810d610b57c27b934425859d158ebd56561bc41f7eeb8795db"},
"gproc": {:hex, :gproc, "0.8.0", "cea02c578589c61e5341fce149ea36ccef236cc2ecac8691fba408e7ea77ec2f", [:rebar3], [], "hexpm", "580adafa56463b75263ef5a5df4c86af321f68694e7786cb057fd805d1e2a7de"},
"grpcbox": {:hex, :grpcbox, "0.16.0", "b83f37c62d6eeca347b77f9b1ec7e9f62231690cdfeb3a31be07cd4002ba9c82", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.13.0", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.8.0", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "294df743ae20a7e030889f00644001370a4f7ce0121f3bbdaf13cf3169c62913"},
"gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"},
"grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm", "06f580167c4b8b8a6429040df36cc93bba6d571faeaec1b28816523379cbb23a"},
"hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"},
"httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
@@ -93,13 +93,13 @@
"sizeable": {:hex, :sizeable, "1.0.2", "625fe06a5dad188b52121a140286f1a6ae1adf350a942cf419499ecd8a11ee29", [:mix], [], "hexpm", "4bab548e6dfba777b400ca50830a9e3a4128e73df77ab1582540cf5860601762"},
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"swoosh": {:hex, :swoosh, "1.14.1", "d8813699ba410509008dd3dfdb2df057e3fce367d45d5e6d76b146a7c9d559cd", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87da72260b4351678f96aec61db5c2acc8a88cda2cf2c4f534eb4c9c461350c7"},
"swoosh": {:hex, :swoosh, "1.14.2", "cf686f92ad3b21e6651b20c50eeb1781f581dc7097ef6251b4d322a9f1d19339", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01d8fae72930a0b5c1bb9725df0408602ed8c5c3d59dc6e7a39c57b723cd1065"},
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"telemetry_registry": {:hex, :telemetry_registry, "0.3.1", "14a3319a7d9027bdbff7ebcacf1a438f5f5c903057b93aee484cca26f05bdcba", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d0ca77b691cf854ed074b459a93b87f4c7f5512f8f7743c635ca83da81f939e"},
"tesla": {:hex, :tesla, "1.7.0", "a62dda2f80d4f8a925eb7b8c5b78c461e0eb996672719fe1a63b26321a5f8b4e", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2e64f01ebfdb026209b47bc651a0e65203fcff4ae79c11efb73c4852b00dc313"},
"tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"},
"tls_certificate_check": {:hex, :tls_certificate_check, "1.20.0", "1ac0c53f95e201feb8d398ef9d764ae74175231289d89f166ba88a7f50cd8e73", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "ab57b74b1a63dc5775650699a3ec032ec0065005eff1f020818742b7312a8426"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"wallaby": {:hex, :wallaby, "0.30.6", "7dc4c1213f3b52c4152581d126632bc7e06892336d3a0f582853efeeabd45a71", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "50950c1d968549b54c20e16175c68c7fc0824138e2bb93feb11ef6add8eb23d4"},