From 4fb101ed9fbfdfb0db1985f424f5388ced539e24 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Mon, 4 Dec 2023 12:56:31 -0600 Subject: [PATCH] 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 Screenshot 2023-12-01 at 18 25 11 Screenshot 2023-12-01 at 18 25 16 Screenshot 2023-12-01 at 18 25 33 Screenshot 2023-12-01 at 18 25 39 Screenshot 2023-12-01 at 18 25 59 --- elixir/apps/domain/lib/domain/auth.ex | 3 +- .../domain/auth/adapters/openid_connect.ex | 2 +- .../lib/domain/auth/provider/changeset.ex | 2 +- elixir/apps/domain/lib/domain/policies.ex | 16 ++ .../apps/domain/lib/domain/policies/policy.ex | 1 + .../lib/domain/policies/policy/changeset.ex | 12 ++ ...201165608_add_policies_disabled_fields.exs | 9 + .../auth/adapters/google_workspace_test.exs | 11 +- .../auth/adapters/openid_connect_test.exs | 11 +- elixir/apps/domain/test/domain/auth_test.exs | 10 +- .../apps/domain/test/domain/policies_test.exs | 111 +++++++++++++ elixir/apps/web/assets/js/hooks.js | 16 +- elixir/apps/web/assets/package.json | 2 +- elixir/apps/web/assets/pnpm-lock.yaml | 8 +- elixir/apps/web/lib/web/auth.ex | 2 + .../web/lib/web/components/core_components.ex | 25 +++ .../web/lib/web/components/form_components.ex | 47 +++++- .../web/lib/web/components/page_components.ex | 2 +- .../lib/web/controllers/home_controller.ex | 18 +- .../apps/web/lib/web/controllers/home_html.ex | 24 ++- elixir/apps/web/lib/web/live/clients/index.ex | 2 + .../apps/web/lib/web/live/policies/index.ex | 11 ++ elixir/apps/web/lib/web/live/policies/show.ex | 46 +++++- .../apps/web/lib/web/live/settings/account.ex | 4 +- .../google_workspace/show.ex | 18 +- .../identity_providers/openid_connect/show.ex | 26 +-- .../identity_providers/system/show.ex | 20 ++- elixir/apps/web/lib/web/live/sign_up.ex | 154 +++++++++++++----- elixir/apps/web/test/web/auth_test.exs | 7 + .../web/test/web/live/policies/show_test.exs | 31 ++++ .../web/live/settings/account/index_test.exs | 2 +- .../google_workspace/show_test.exs | 11 +- .../openid_connect/show_test.exs | 10 +- .../identity_providers/system/show_test.exs | 11 +- elixir/mix.lock | 26 +-- 35 files changed, 589 insertions(+), 122 deletions(-) create mode 100644 elixir/apps/domain/priv/repo/migrations/20231201165608_add_policies_disabled_fields.exs diff --git a/elixir/apps/domain/lib/domain/auth.ex b/elixir/apps/domain/lib/domain/auth.ex index 95a2ef0e0..235efb27d 100644 --- a/elixir/apps/domain/lib/domain/auth.ex +++ b/elixir/apps/domain/lib/domain/auth.ex @@ -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 diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex index 9c39e14f8..812c7e92a 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex @@ -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 diff --git a/elixir/apps/domain/lib/domain/auth/provider/changeset.ex b/elixir/apps/domain/lib/domain/auth/provider/changeset.ex index 80135d7a4..6fa31d83c 100644 --- a/elixir/apps/domain/lib/domain/auth/provider/changeset.ex +++ b/elixir/apps/domain/lib/domain/auth/provider/changeset.ex @@ -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 diff --git a/elixir/apps/domain/lib/domain/policies.ex b/elixir/apps/domain/lib/domain/policies.ex index 1c961aa9b..161b7ae4c 100644 --- a/elixir/apps/domain/lib/domain/policies.ex +++ b/elixir/apps/domain/lib/domain/policies.ex @@ -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()]} diff --git a/elixir/apps/domain/lib/domain/policies/policy.ex b/elixir/apps/domain/lib/domain/policies/policy.ex index c4babba2f..8bedfa1b9 100644 --- a/elixir/apps/domain/lib/domain/policies/policy.ex +++ b/elixir/apps/domain/lib/domain/policies/policy.ex @@ -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 diff --git a/elixir/apps/domain/lib/domain/policies/policy/changeset.ex b/elixir/apps/domain/lib/domain/policies/policy/changeset.ex index 98dca1bee..8c64ffa43 100644 --- a/elixir/apps/domain/lib/domain/policies/policy/changeset.ex +++ b/elixir/apps/domain/lib/domain/policies/policy/changeset.ex @@ -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() diff --git a/elixir/apps/domain/priv/repo/migrations/20231201165608_add_policies_disabled_fields.exs b/elixir/apps/domain/priv/repo/migrations/20231201165608_add_policies_disabled_fields.exs new file mode 100644 index 000000000..153f29431 --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20231201165608_add_policies_disabled_fields.exs @@ -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 diff --git a/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs b/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs index fd48d5f0f..6ec1265cc 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/google_workspace_test.exs @@ -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 diff --git a/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs b/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs index 7640306b7..390bf3035 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/openid_connect_test.exs @@ -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 diff --git a/elixir/apps/domain/test/domain/auth_test.exs b/elixir/apps/domain/test/domain/auth_test.exs index 23c7c11b4..7afcd3507 100644 --- a/elixir/apps/domain/test/domain/auth_test.exs +++ b/elixir/apps/domain/test/domain/auth_test.exs @@ -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", %{ diff --git a/elixir/apps/domain/test/domain/policies_test.exs b/elixir/apps/domain/test/domain/policies_test.exs index de69daae6..66dfff8b5 100644 --- a/elixir/apps/domain/test/domain/policies_test.exs +++ b/elixir/apps/domain/test/domain/policies_test.exs @@ -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 = diff --git a/elixir/apps/web/assets/js/hooks.js b/elixir/apps/web/assets/js/hooks.js index 6a44cf0d8..45a226ec7 100644 --- a/elixir/apps/web/assets/js/hooks.js +++ b/elixir/apps/web/assets/js/hooks.js @@ -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); }); }, diff --git a/elixir/apps/web/assets/package.json b/elixir/apps/web/assets/package.json index 028140d5c..bc3b37c2d 100644 --- a/elixir/apps/web/assets/package.json +++ b/elixir/apps/web/assets/package.json @@ -1,6 +1,6 @@ { "dependencies": { "@fontsource/source-sans-pro": "^5.0.8", - "flowbite": "^2.1.1" + "flowbite": "^2.2.0" } } diff --git a/elixir/apps/web/assets/pnpm-lock.yaml b/elixir/apps/web/assets/pnpm-lock.yaml index 2e2f8b18a..14270a826 100644 --- a/elixir/apps/web/assets/pnpm-lock.yaml +++ b/elixir/apps/web/assets/pnpm-lock.yaml @@ -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 diff --git a/elixir/apps/web/lib/web/auth.ex b/elixir/apps/web/lib/web/auth.ex index 7ff426ab1..9aaca9310 100644 --- a/elixir/apps/web/lib/web/auth.ex +++ b/elixir/apps/web/lib/web/auth.ex @@ -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))) diff --git a/elixir/apps/web/lib/web/components/core_components.ex b/elixir/apps/web/lib/web/components/core_components.ex index 1aab8de38..14cb29adf 100644 --- a/elixir/apps/web/lib/web/components/core_components.ex +++ b/elixir/apps/web/lib/web/components/core_components.ex @@ -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. + + """ + attr :id, :string, required: true + attr :class, :string, default: "" + slot :inner_block, required: true + attr :rest, :global + + def copy(assigns) do + ~H""" +
+ <%= render_slot(@inner_block) %> + + <.icon name="hero-clipboard-document" data-icon class="h-4 w-4" /> + +
+ """ + end + @doc """ Render a tabs toggle container and its content. diff --git a/elixir/apps/web/lib/web/components/form_components.ex b/elixir/apps/web/lib/web/components/form_components.ex index 055e9ba5d..6fc44f870 100644 --- a/elixir/apps/web/lib/web/components/form_components.ex +++ b/elixir/apps/web/lib/web/components/form_components.ex @@ -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""" +
+ <.label :if={not is_nil(@label)} for={@id}><%= @label %> +
+
+ <%= @prefix %> +
+ +
+ <.error :for={msg <- @errors} data-validation-error-for={@name}><%= msg %> +
+ """ + end + def input(assigns) do ~H"""
diff --git a/elixir/apps/web/lib/web/components/page_components.ex b/elixir/apps/web/lib/web/components/page_components.ex index 1edd15898..14135abe0 100644 --- a/elixir/apps/web/lib/web/components/page_components.ex +++ b/elixir/apps/web/lib/web/components/page_components.ex @@ -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 diff --git a/elixir/apps/web/lib/web/controllers/home_controller.ex b/elixir/apps/web/lib/web/controllers/home_controller.ex index 7e4754e3a..f69dcdb96 100644 --- a/elixir/apps/web/lib/web/controllers/home_controller.ex +++ b/elixir/apps/web/lib/web/controllers/home_controller.ex @@ -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 diff --git a/elixir/apps/web/lib/web/controllers/home_html.ex b/elixir/apps/web/lib/web/controllers/home_html.ex index 9a89186b2..5263bb426 100644 --- a/elixir/apps/web/lib/web/controllers/home_html.ex +++ b/elixir/apps/web/lib/web/controllers/home_html.ex @@ -21,25 +21,39 @@ defmodule Web.HomeHTML do
- <.account_button :for={account <- @accounts} account={account} /> + <.account_button + :for={account <- @accounts} + account={account} + redirect_params={@redirect_params} + />
<.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 /> +

As shown in your "Welcome to Firezone" email

<.button class="w-full"> Go to Sign In page -

+

Don't have an account? Sign up here. @@ -54,7 +68,7 @@ defmodule Web.HomeHTML do def account_button(assigns) do ~H""" - diff --git a/elixir/apps/web/lib/web/live/policies/index.ex b/elixir/apps/web/lib/web/live/policies/index.ex index b023245d8..77fa7d49f 100644 --- a/elixir/apps/web/lib/web/live/policies/index.ex +++ b/elixir/apps/web/lib/web/live/policies/index.ex @@ -42,6 +42,17 @@ defmodule Web.Policies.Index do <%= policy.resource.name %> + <: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 %> + <:empty>

diff --git a/elixir/apps/web/lib/web/live/policies/show.ex b/elixir/apps/web/lib/web/live/policies/show.ex index 3df3050bc..fecef894b 100644 --- a/elixir/apps/web/lib/web/live/policies/show.ex +++ b/elixir/apps/web/lib/web/live/policies/show.ex @@ -38,6 +38,7 @@ defmodule Web.Policies.Show do <.section> <:title> <%= @page_title %>: <%= @policy.id %> + (disabled) (deleted) <:action :if={is_nil(@policy.deleted_at)}> @@ -45,6 +46,22 @@ defmodule Web.Policies.Show do Edit Policy + <: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 + :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 + + <:content> <.vertical_table id="policy"> <.vertical_table_row> @@ -157,12 +174,37 @@ defmodule Web.Policies.Show do Delete Policy - <:content> """ 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 diff --git a/elixir/apps/web/lib/web/live/settings/account.ex b/elixir/apps/web/lib/web/live/settings/account.ex index 1139cc284..46c6c5dfe 100644 --- a/elixir/apps/web/lib/web/live/settings/account.ex +++ b/elixir/apps/web/lib/web/live/settings/account.ex @@ -24,7 +24,9 @@ defmodule Web.Settings.Account do <.vertical_table_row> <:label>Account Slug - <:value><%= @account.slug %> + <:value> + <.copy id="account-slug"><%= @account.slug %> +
diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/google_workspace/show.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/google_workspace/show.ex index 2b1f57a83..a8a00204a 100644 --- a/elixir/apps/web/lib/web/live/settings/identity_providers/google_workspace/show.ex +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/google_workspace/show.ex @@ -29,29 +29,35 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Show do <.section> <:title> Identity Provider <%= @provider.name %> + (disabled) + (deleted) - <:action> + <:action :if={is_nil(@provider.deleted_at)}> <.edit_button navigate={ ~p"/#{@account}/settings/identity_providers/google_workspace/#{@provider.id}/edit" }> Edit - <: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 :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 <% end %> - <:action> + <:action :if={is_nil(@provider.deleted_at)}> <.button style="primary" navigate={ @@ -96,7 +102,7 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Show do - <.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?" diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/openid_connect/show.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/openid_connect/show.ex index 9992b0547..c4b2826eb 100644 --- a/elixir/apps/web/lib/web/live/settings/identity_providers/openid_connect/show.ex +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/openid_connect/show.ex @@ -28,8 +28,10 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Show do <.section> <:title> Identity Provider <%= @provider.name %> + (disabled) + (deleted) - <: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 - <: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 - <: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 - <:action> + <:action :if={is_nil(@provider.deleted_at)}> <.button style="primary" navigate={ @@ -115,10 +121,7 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Show do
- <.section> - <:title> - 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?" @@ -127,8 +130,7 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Show do Delete Identity Provider - <:content> - + """ end diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/system/show.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/system/show.ex index 3aa3899e7..9df83682a 100644 --- a/elixir/apps/web/lib/web/live/settings/identity_providers/system/show.ex +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/system/show.ex @@ -31,15 +31,21 @@ defmodule Web.Settings.IdentityProviders.System.Show do <.section> <:title> Identity Provider <%= @provider.name %> + (disabled) + (deleted) - <: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 :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 @@ -74,10 +80,7 @@ defmodule Web.Settings.IdentityProviders.System.Show do - <.section> - <:title> - 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?" @@ -86,8 +89,7 @@ defmodule Web.Settings.IdentityProviders.System.Show do Delete Identity Provider - <:content> - + """ end diff --git a/elixir/apps/web/lib/web/live/sign_up.ex b/elixir/apps/web/lib/web/live/sign_up.ex index 171c94675..48d9f1456 100644 --- a/elixir/apps/web/lib/web/live/sign_up.ex +++ b/elixir/apps/web/lib/web/live/sign_up.ex @@ -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?} /> @@ -101,41 +109,61 @@ defmodule Web.SignUp do ~H"""
- Your account has been created! Please check your email for sign in instructions. + Your account has been created! +

Please check your email for sign in instructions.

- +
- - - - + + + +
+ Account Name: + <%= @account.name %>
+ Account Slug: + <%= @account.slug %>
+ Sign In URL: + + <.link class="font-medium text-blue-600 hover:underline" navigate={~p"/#{@account}"}> + <%= url(~p"/#{@account}") %> + +
-
- Sign In URL -
-
- <.link class="font-medium text-blue-600 hover:underline" navigate={~p"/#{@account.slug}"}> - <%= "#{Web.Endpoint.url()}/#{@account.slug}" %> - -
+ <.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 + +
""" @@ -147,12 +175,22 @@ defmodule Web.SignUp do Sign Up Now <.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" /> - <.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 diff --git a/elixir/apps/web/test/web/auth_test.exs b/elixir/apps/web/test/web/auth_test.exs index 26beb2192..482e5ef68 100644 --- a/elixir/apps/web/test/web/auth_test.exs +++ b/elixir/apps/web/test/web/auth_test.exs @@ -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", %{ diff --git a/elixir/apps/web/test/web/live/policies/show_test.exs b/elixir/apps/web/test/web/live/policies/show_test.exs index ebf8bb6e3..ea85c5c99 100644 --- a/elixir/apps/web/test/web/live/policies/show_test.exs +++ b/elixir/apps/web/test/web/live/policies/show_test.exs @@ -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 diff --git a/elixir/apps/web/test/web/live/settings/account/index_test.exs b/elixir/apps/web/test/web/live/settings/account/index_test.exs index 2d007bf3f..d45eb4532 100644 --- a/elixir/apps/web/test/web/live/settings/account/index_test.exs +++ b/elixir/apps/web/test/web/live/settings/account/index_test.exs @@ -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 diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/show_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/show_test.exs index de62a709a..1aaac87d3 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/show_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/google_workspace/show_test.exs @@ -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", %{ diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/show_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/show_test.exs index 79b0163e2..867cb2319 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/show_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/openid_connect/show_test.exs @@ -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", %{ diff --git a/elixir/apps/web/test/web/live/settings/identity_providers/system/show_test.exs b/elixir/apps/web/test/web/live/settings/identity_providers/system/show_test.exs index d05a1a674..57a4d4188 100644 --- a/elixir/apps/web/test/web/live/settings/identity_providers/system/show_test.exs +++ b/elixir/apps/web/test/web/live/settings/identity_providers/system/show_test.exs @@ -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", %{ diff --git a/elixir/mix.lock b/elixir/mix.lock index 272c036fd..1b8d0c3c0 100644 --- a/elixir/mix.lock +++ b/elixir/mix.lock @@ -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"},