diff --git a/elixir/README.md b/elixir/README.md
index 3dd1c8b60..01f608a4a 100644
--- a/elixir/README.md
+++ b/elixir/README.md
@@ -418,7 +418,7 @@ iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)5> context = %Domain.Aut
iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)6> {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context)
{:ok, ...}
-iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)7> Web.Mailer.AuthEmail.sign_in_link_email(identity) |> Web.Mailer.deliver()
+iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)7> Domain.Mailer.AuthEmail.sign_in_link_email(identity) |> Domain.Mailer.deliver()
{:ok, %{id: "d24dbe9a-d0f5-4049-ac0d-0df793725a80"}}
```
diff --git a/elixir/apps/api/lib/api/plugs/auth.ex b/elixir/apps/api/lib/api/plugs/auth.ex
index 73140e140..7d5868ce1 100644
--- a/elixir/apps/api/lib/api/plugs/auth.ex
+++ b/elixir/apps/api/lib/api/plugs/auth.ex
@@ -11,12 +11,6 @@ defmodule API.Plugs.Auth do
assign(conn, :subject, subject)
else
_ ->
- # conn
- # |> put_resp_content_type("application/json")
- # |> send_resp(401, Jason.encode!(%{"error" => "invalid_access_token"}))
- # |> halt()
-
- # TODO: BRIAN - Confirm that this change won't break anything with the clients or gateways
conn
|> put_status(401)
|> Phoenix.Controller.put_view(json: API.ErrorJSON)
diff --git a/elixir/apps/domain/.formatter.exs b/elixir/apps/domain/.formatter.exs
index 9819eca3c..0e0518ee2 100644
--- a/elixir/apps/domain/.formatter.exs
+++ b/elixir/apps/domain/.formatter.exs
@@ -1,7 +1,8 @@
[
import_deps: [
:ecto,
- :plug
+ :plug,
+ :phoenix
],
inputs: [
"*.{heex,ex,exs}",
diff --git a/elixir/apps/domain/lib/domain/actors.ex b/elixir/apps/domain/lib/domain/actors.ex
index a05571057..2dc83b11f 100644
--- a/elixir/apps/domain/lib/domain/actors.ex
+++ b/elixir/apps/domain/lib/domain/actors.ex
@@ -402,6 +402,16 @@ defmodule Domain.Actors do
end)
end
+ def all_admins_for_account!(%Accounts.Account{} = account, opts \\ []) do
+ {preload, _opts} = Keyword.pop(opts, :preload, [])
+
+ Actor.Query.not_disabled()
+ |> Actor.Query.by_account_id(account.id)
+ |> Actor.Query.by_type(:account_admin_user)
+ |> Repo.all(opts)
+ |> Repo.preload(preload)
+ end
+
def list_actors(%Auth.Subject{} = subject, opts \\ []) do
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do
Actor.Query.not_deleted()
diff --git a/elixir/apps/domain/lib/domain/application.ex b/elixir/apps/domain/lib/domain/application.ex
index a0d5d7c99..798d5dc44 100644
--- a/elixir/apps/domain/lib/domain/application.ex
+++ b/elixir/apps/domain/lib/domain/application.ex
@@ -30,6 +30,8 @@ defmodule Domain.Application do
Domain.Gateways,
Domain.Clients,
Domain.Billing,
+ Domain.Mailer,
+ Domain.Mailer.RateLimiter,
# Observability
Domain.Telemetry
diff --git a/elixir/apps/domain/lib/domain/auth.ex b/elixir/apps/domain/lib/domain/auth.ex
index 4ebb9e870..02b98dff0 100644
--- a/elixir/apps/domain/lib/domain/auth.ex
+++ b/elixir/apps/domain/lib/domain/auth.ex
@@ -352,6 +352,12 @@ defmodule Domain.Auth do
end
end
+ def all_identities_for(%Actors.Actor{} = actor, opts \\ []) do
+ Identity.Query.not_deleted()
+ |> Identity.Query.by_actor_id(actor.id)
+ |> Repo.all(opts)
+ end
+
def list_identities_for(%Actors.Actor{} = actor, %Subject{} = subject, opts \\ []) do
with :ok <- ensure_has_permissions(subject, Authorizer.manage_identities_permission()) do
Identity.Query.not_deleted()
@@ -373,6 +379,19 @@ defmodule Domain.Auth do
|> Repo.all()
end
+ def get_identity_email(%Identity{} = identity) do
+ provider_email(identity) || identity.provider_identifier
+ end
+
+ def identity_has_email?(%Identity{} = identity) do
+ not is_nil(provider_email(identity)) or identity.provider.adapter == :email or
+ identity.provider_identifier =~ "@"
+ end
+
+ defp provider_email(%Identity{} = identity) do
+ get_in(identity.provider_state, ["userinfo", "email"])
+ end
+
# used by IdP adapters
def upsert_identity(%Actors.Actor{} = actor, %Provider{} = provider, attrs) do
Identity.Changeset.create_identity(actor, provider, attrs)
diff --git a/elixir/apps/domain/lib/domain/auth/adapter/openid_connect/directory_sync.ex b/elixir/apps/domain/lib/domain/auth/adapter/openid_connect/directory_sync.ex
index d4455cd59..1fba3c752 100644
--- a/elixir/apps/domain/lib/domain/auth/adapter/openid_connect/directory_sync.ex
+++ b/elixir/apps/domain/lib/domain/auth/adapter/openid_connect/directory_sync.ex
@@ -209,6 +209,7 @@ defmodule Domain.Auth.Adapter.OpenIDConnect.DirectorySync do
Auth.Provider.Changeset.sync_requires_manual_intervention(provider, user_message)
|> Domain.Repo.update!()
+ |> send_sync_error_email()
:error
@@ -224,6 +225,7 @@ defmodule Domain.Auth.Adapter.OpenIDConnect.DirectorySync do
Auth.Provider.Changeset.sync_failed(provider, user_message)
|> Domain.Repo.update!()
+ |> send_sync_error_email()
|> log_sync_error(log_message)
:error
@@ -382,6 +384,37 @@ defmodule Domain.Auth.Adapter.OpenIDConnect.DirectorySync do
end)
end
+ defp send_sync_error_email(provider) do
+ provider = Repo.preload(provider, :account)
+
+ if sync_error_email_sent_today?(provider) do
+ Logger.debug("Sync error email already sent today")
+
+ provider
+ else
+ Domain.Actors.all_admins_for_account!(provider.account, preload: :identities)
+ |> Enum.flat_map(fn actor ->
+ Enum.map(actor.identities, &Domain.Auth.get_identity_email(&1))
+ end)
+ |> Enum.uniq()
+ |> Enum.each(fn email ->
+ Domain.Mailer.SyncEmail.sync_error_email(provider, email)
+ |> Domain.Mailer.deliver()
+ end)
+
+ Auth.Provider.Changeset.sync_error_emailed(provider)
+ |> Domain.Repo.update!()
+ end
+ end
+
+ defp sync_error_email_sent_today?(provider) do
+ if last_email_time = provider.sync_error_emailed_at do
+ DateTime.diff(DateTime.utc_now(), last_email_time, :hour) < 24
+ else
+ false
+ end
+ end
+
if Mix.env() == :test do
# We need this function to reuse the connection that was checked out in a parent process.
#
diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta/api_client.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta/api_client.ex
index ebbf87e0b..f96f93bde 100644
--- a/elixir/apps/domain/lib/domain/auth/adapters/okta/api_client.ex
+++ b/elixir/apps/domain/lib/domain/auth/adapters/okta/api_client.ex
@@ -100,6 +100,7 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do
end
end
+ # TODO: Need to catch 401/403 specifically when error message is in header
defp list(uri, headers, api_token) do
headers = headers ++ [{"Authorization", "Bearer #{api_token}"}]
request = Finch.build(:get, uri, headers)
diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex
index ba5c9fa0a..4c13e58ee 100644
--- a/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex
+++ b/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex
@@ -46,6 +46,16 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectory do
message = "#{error_code} => #{error_summary}"
{:error, message, "Okta API returned #{status}: #{message}"}
+ # TODO: Okta API client needs to be updated to pull message from header
+ {:error, {401, ""}} ->
+ message = "401 - Unauthorized"
+ {:error, message, message}
+
+ # TODO: Okta API client needs to be updated to pull message from header
+ {:error, {403, ""}} ->
+ message = "403 - Forbidden"
+ {:error, message, message}
+
{:error, :retry_later} ->
message = "Okta API is temporarily unavailable"
{:error, message, message}
diff --git a/elixir/apps/domain/lib/domain/auth/provider.ex b/elixir/apps/domain/lib/domain/auth/provider.ex
index 514638b2a..52bcc83d9 100644
--- a/elixir/apps/domain/lib/domain/auth/provider.ex
+++ b/elixir/apps/domain/lib/domain/auth/provider.ex
@@ -23,6 +23,7 @@ defmodule Domain.Auth.Provider do
field :last_sync_error, :string
field :last_synced_at, :utc_datetime_usec
field :sync_disabled_at, :utc_datetime_usec
+ field :sync_error_emailed_at, :utc_datetime_usec
field :disabled_at, :utc_datetime_usec
field :deleted_at, :utc_datetime_usec
diff --git a/elixir/apps/domain/lib/domain/auth/provider/changeset.ex b/elixir/apps/domain/lib/domain/auth/provider/changeset.ex
index 2b3165eb0..f939ee54d 100644
--- a/elixir/apps/domain/lib/domain/auth/provider/changeset.ex
+++ b/elixir/apps/domain/lib/domain/auth/provider/changeset.ex
@@ -5,7 +5,7 @@ defmodule Domain.Auth.Provider.Changeset do
@create_fields ~w[id name adapter provisioner adapter_config adapter_state disabled_at]a
@update_fields ~w[name adapter_config
- last_syncs_failed last_sync_error sync_disabled_at
+ last_syncs_failed last_sync_error sync_disabled_at sync_error_emailed_at
adapter_state provisioner disabled_at deleted_at]a
@required_fields ~w[name adapter adapter_config provisioner]a
@@ -47,9 +47,9 @@ defmodule Domain.Auth.Provider.Changeset do
provider
|> change()
|> put_change(:last_synced_at, DateTime.utc_now())
- |> put_change(:last_sync_error, nil)
|> put_change(:last_syncs_failed, 0)
|> put_change(:sync_disabled_at, nil)
+ |> put_change(:sync_error_emailed_at, nil)
end
def sync_failed(%Provider{} = provider, error) do
@@ -62,6 +62,12 @@ defmodule Domain.Auth.Provider.Changeset do
|> put_change(:last_syncs_failed, last_syncs_failed + 1)
end
+ def sync_error_emailed(%Provider{} = provider) do
+ provider
+ |> change()
+ |> put_change(:sync_error_emailed_at, DateTime.utc_now())
+ end
+
def sync_requires_manual_intervention(%Provider{} = provider, error) do
sync_failed(provider, error)
|> put_change(:sync_disabled_at, DateTime.utc_now())
diff --git a/elixir/apps/domain/lib/domain/cldr.ex b/elixir/apps/domain/lib/domain/cldr.ex
new file mode 100644
index 000000000..7c67c4c7a
--- /dev/null
+++ b/elixir/apps/domain/lib/domain/cldr.ex
@@ -0,0 +1,5 @@
+defmodule Domain.CLDR do
+ use Cldr,
+ locales: ["en"],
+ providers: [Cldr.Number, Cldr.Calendar, Cldr.DateTime]
+end
diff --git a/elixir/apps/web/lib/web/mailer.ex b/elixir/apps/domain/lib/domain/mailer.ex
similarity index 91%
rename from elixir/apps/web/lib/web/mailer.ex
rename to elixir/apps/domain/lib/domain/mailer.ex
index e6b708dac..44b1c7dea 100644
--- a/elixir/apps/web/lib/web/mailer.ex
+++ b/elixir/apps/domain/lib/domain/mailer.ex
@@ -1,8 +1,8 @@
-defmodule Web.Mailer do
+defmodule Domain.Mailer do
use Supervisor
alias Swoosh.Mailer
alias Swoosh.Email
- alias Web.Mailer.RateLimiter
+ alias Domain.Mailer.RateLimiter
require Logger
def start_link(arg) do
@@ -42,7 +42,7 @@ defmodule Web.Mailer do
custom adapter implementation that does nothing.
"""
def deliver(email, config \\ []) do
- opts = Mailer.parse_config(:web, __MODULE__, [], config)
+ opts = Mailer.parse_config(:domain, __MODULE__, [], config)
metadata = %{email: email, config: config, mailer: __MODULE__}
if opts[:adapter] do
@@ -80,14 +80,14 @@ defmodule Web.Mailer do
end
def active? do
- mailer_config = Domain.Config.fetch_env!(:web, Web.Mailer)
+ mailer_config = Domain.Config.fetch_env!(:domain, Domain.Mailer)
mailer_config[:from_email] && mailer_config[:adapter]
end
def default_email do
# Fail hard if email not configured
from_email =
- Domain.Config.fetch_env!(:web, Web.Mailer)
+ Domain.Config.fetch_env!(:domain, Domain.Mailer)
|> Keyword.fetch!(:from_email)
Email.new()
diff --git a/elixir/apps/web/lib/web/mailer/auth_email.ex b/elixir/apps/domain/lib/domain/mailer/auth_email.ex
similarity index 71%
rename from elixir/apps/web/lib/web/mailer/auth_email.ex
rename to elixir/apps/domain/lib/domain/mailer/auth_email.ex
index 6675e99bf..e7c604597 100644
--- a/elixir/apps/web/lib/web/mailer/auth_email.ex
+++ b/elixir/apps/domain/lib/domain/mailer/auth_email.ex
@@ -1,7 +1,7 @@
-defmodule Web.Mailer.AuthEmail do
- use Web, :html
+defmodule Domain.Mailer.AuthEmail do
import Swoosh.Email
- import Web.Mailer
+ import Domain.Mailer
+ import Phoenix.Template, only: [embed_templates: 2]
embed_templates "auth_email/*.html", suffix: "_html"
embed_templates "auth_email/*.text", suffix: "_text"
@@ -12,7 +12,7 @@ defmodule Web.Mailer.AuthEmail do
user_agent,
remote_ip
) do
- sign_in_form_url = url(~p"/#{account}")
+ sign_in_form_url = url("/#{account.slug}")
default_email()
|> subject("Welcome to Firezone")
@@ -40,11 +40,12 @@ defmodule Web.Mailer.AuthEmail do
sign_in_url =
url(
- ~p"/#{identity.account}/sign_in/providers/#{identity.provider_id}/verify_sign_in_token?#{params}"
+ "/#{identity.account.slug}/sign_in/providers/#{identity.provider_id}/verify_sign_in_token",
+ params
)
sign_in_token_created_at =
- Cldr.DateTime.to_string!(identity.provider_state["token_created_at"], Web.CLDR,
+ Cldr.DateTime.to_string!(identity.provider_state["token_created_at"], Domain.CLDR,
format: :short
) <> " UTC"
@@ -69,11 +70,27 @@ defmodule Web.Mailer.AuthEmail do
) do
default_email()
|> subject("Welcome to Firezone")
- |> to(get_identity_email(identity))
+ |> to(Domain.Auth.get_identity_email(identity))
|> render_body(__MODULE__, :new_user,
account: account,
identity: identity,
subject: subject
)
end
+
+ def url(path, params \\ %{}) do
+ Domain.Config.fetch_env!(:domain, :web_external_url)
+ |> URI.parse()
+ |> URI.append_path(path)
+ |> maybe_append_query(params)
+ |> URI.to_string()
+ end
+
+ def maybe_append_query(uri, params) do
+ if Enum.empty?(params) do
+ uri
+ else
+ URI.append_query(uri, URI.encode_query(params))
+ end
+ end
end
diff --git a/elixir/apps/web/lib/web/mailer/auth_email/new_user.html.heex b/elixir/apps/domain/lib/domain/mailer/auth_email/new_user.html.eex
similarity index 100%
rename from elixir/apps/web/lib/web/mailer/auth_email/new_user.html.heex
rename to elixir/apps/domain/lib/domain/mailer/auth_email/new_user.html.eex
diff --git a/elixir/apps/web/lib/web/mailer/auth_email/new_user.text.heex b/elixir/apps/domain/lib/domain/mailer/auth_email/new_user.text.eex
similarity index 100%
rename from elixir/apps/web/lib/web/mailer/auth_email/new_user.text.heex
rename to elixir/apps/domain/lib/domain/mailer/auth_email/new_user.text.eex
diff --git a/elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.html.heex b/elixir/apps/domain/lib/domain/mailer/auth_email/sign_in_link.html.eex
similarity index 100%
rename from elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.html.heex
rename to elixir/apps/domain/lib/domain/mailer/auth_email/sign_in_link.html.eex
diff --git a/elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.text.heex b/elixir/apps/domain/lib/domain/mailer/auth_email/sign_in_link.text.eex
similarity index 100%
rename from elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.text.heex
rename to elixir/apps/domain/lib/domain/mailer/auth_email/sign_in_link.text.eex
diff --git a/elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.html.heex b/elixir/apps/domain/lib/domain/mailer/auth_email/sign_up_link.html.eex
similarity index 100%
rename from elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.html.heex
rename to elixir/apps/domain/lib/domain/mailer/auth_email/sign_up_link.html.eex
diff --git a/elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.text.heex b/elixir/apps/domain/lib/domain/mailer/auth_email/sign_up_link.text.eex
similarity index 100%
rename from elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.text.heex
rename to elixir/apps/domain/lib/domain/mailer/auth_email/sign_up_link.text.eex
diff --git a/elixir/apps/web/lib/web/mailer/beta_email.ex b/elixir/apps/domain/lib/domain/mailer/beta_email.ex
similarity index 85%
rename from elixir/apps/web/lib/web/mailer/beta_email.ex
rename to elixir/apps/domain/lib/domain/mailer/beta_email.ex
index 8696bf630..d55b0fa21 100644
--- a/elixir/apps/web/lib/web/mailer/beta_email.ex
+++ b/elixir/apps/domain/lib/domain/mailer/beta_email.ex
@@ -1,7 +1,7 @@
-defmodule Web.Mailer.BetaEmail do
- use Web, :html
+defmodule Domain.Mailer.BetaEmail do
import Swoosh.Email
- import Web.Mailer
+ import Domain.Mailer
+ import Phoenix.Template, only: [embed_templates: 2]
embed_templates "beta_email/*.text", suffix: "_text"
diff --git a/elixir/apps/web/lib/web/mailer/beta_email/rest_api_request.text.heex b/elixir/apps/domain/lib/domain/mailer/beta_email/rest_api_request.text.eex
similarity index 100%
rename from elixir/apps/web/lib/web/mailer/beta_email/rest_api_request.text.heex
rename to elixir/apps/domain/lib/domain/mailer/beta_email/rest_api_request.text.eex
diff --git a/elixir/apps/web/lib/web/mailer/rate_limiter.ex b/elixir/apps/domain/lib/domain/mailer/rate_limiter.ex
similarity index 98%
rename from elixir/apps/web/lib/web/mailer/rate_limiter.ex
rename to elixir/apps/domain/lib/domain/mailer/rate_limiter.ex
index 15930a063..3c5e553e7 100644
--- a/elixir/apps/web/lib/web/mailer/rate_limiter.ex
+++ b/elixir/apps/domain/lib/domain/mailer/rate_limiter.ex
@@ -1,4 +1,4 @@
-defmodule Web.Mailer.RateLimiter do
+defmodule Domain.Mailer.RateLimiter do
use GenServer
@default_ets_table_name __MODULE__.ETS
diff --git a/elixir/apps/domain/lib/domain/mailer/sync_email.ex b/elixir/apps/domain/lib/domain/mailer/sync_email.ex
new file mode 100644
index 000000000..82c177bd2
--- /dev/null
+++ b/elixir/apps/domain/lib/domain/mailer/sync_email.ex
@@ -0,0 +1,15 @@
+defmodule Domain.Mailer.SyncEmail do
+ import Swoosh.Email
+ import Domain.Mailer
+ import Phoenix.Template, only: [embed_templates: 2]
+
+ embed_templates "sync_email/*.html", suffix: "_html"
+ embed_templates "sync_email/*.text", suffix: "_text"
+
+ def sync_error_email(%Domain.Auth.Provider{} = provider, email) do
+ default_email()
+ |> subject("Firezone Identity Provider Sync Error")
+ |> to(email)
+ |> render_body(__MODULE__, :sync_error, account: provider.account, provider: provider)
+ end
+end
diff --git a/elixir/apps/domain/lib/domain/mailer/sync_email/sync_error.html.eex b/elixir/apps/domain/lib/domain/mailer/sync_email/sync_error.html.eex
new file mode 100644
index 000000000..a6085469e
--- /dev/null
+++ b/elixir/apps/domain/lib/domain/mailer/sync_email/sync_error.html.eex
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+ Firezone Sync Error
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= @provider.name %> Sync Error!
+
+
+ <%= @provider.name %> has failed to sync <%= @provider.last_syncs_failed %> times.
+
+
+
+ Below is the last sync error message:
+
+ <%= @provider.last_sync_error %>
+
+
+
+
+ Identity Provider Details
+
+
+
+ | Account |
+ <%= @account.name %> |
+
+
+ | Account ID |
+ <%= @account.id %> |
+
+
+ | Provider Name |
+ <%= @provider.name %> |
+
+
+ | Provider ID |
+ <%= @provider.id %> |
+
+
+
+
+
+ Please verify that all Identity Provider information entered in to Firezone is correct. If the problem persists, please reach out to Firezone support
+ using Slack or email.
+
+
+ Thanks, The Firezone Team
+
+
+ |
+
+
+ | |
+
+
+ |
+
+ Blazing-fast alternative to legacy VPNs
+
+
+ Docs
+ •
+ Github
+ •
+ X
+
+ |
+
+
+ |
+
+
+
+
+
+
diff --git a/elixir/apps/domain/lib/domain/mailer/sync_email/sync_error.text.eex b/elixir/apps/domain/lib/domain/mailer/sync_email/sync_error.text.eex
new file mode 100644
index 000000000..18d80e66e
--- /dev/null
+++ b/elixir/apps/domain/lib/domain/mailer/sync_email/sync_error.text.eex
@@ -0,0 +1,12 @@
+Identity Provider Sync Error!
+
+An Identity Provider in your Firezone Account has failed to sync <%= @provider.last_syncs_failed%> times.
+
+The following is the last sync error message:
+<%= @provider.last_sync_error %>
+
+Identity Provider details:
+ Account: <%= @account.name %>
+ Account ID: <%= @account.id %>
+ Provider Name: <%= @provider.name %>
+ Provider ID: <%= @provider.id %>
diff --git a/elixir/apps/domain/mix.exs b/elixir/apps/domain/mix.exs
index d5e59795f..b52edde51 100644
--- a/elixir/apps/domain/mix.exs
+++ b/elixir/apps/domain/mix.exs
@@ -64,6 +64,18 @@ defmodule Domain.MixProject do
# Erlang Clustering
{:libcluster, "~> 3.3"},
+ # CLDR and unit conversions
+ {:ex_cldr_dates_times, "~> 2.13"},
+ {:ex_cldr_numbers, "~> 2.31"},
+ {:ex_cldr, "~> 2.38"},
+
+ # Mailer deps
+ {:gen_smtp, "~> 1.0"},
+ {:multipart, "~> 0.4.0"},
+ {:phoenix_html, "~> 4.0"},
+ {:phoenix_swoosh, "~> 1.0"},
+ {:phoenix_template, "~> 1.0.4"},
+
# Observability and Runtime debugging
{:bandit, "~> 1.0"},
{:plug, "~> 1.15"},
diff --git a/elixir/apps/domain/priv/repo/migrations/20240823195642_add_sync_error_email_to_provider.exs b/elixir/apps/domain/priv/repo/migrations/20240823195642_add_sync_error_email_to_provider.exs
new file mode 100644
index 000000000..7bc98f30a
--- /dev/null
+++ b/elixir/apps/domain/priv/repo/migrations/20240823195642_add_sync_error_email_to_provider.exs
@@ -0,0 +1,9 @@
+defmodule Domain.Repo.Migrations.AddSyncErrorEmailToProvider do
+ use Ecto.Migration
+
+ def change do
+ alter table(:auth_providers) do
+ add(:sync_error_emailed_at, :utc_datetime_usec)
+ end
+ end
+end
diff --git a/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/sync_directory_test.exs b/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/sync_directory_test.exs
index 1ac3e3763..cb9959996 100644
--- a/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/sync_directory_test.exs
+++ b/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs/sync_directory_test.exs
@@ -774,5 +774,75 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectoryTest do
cancel_bypass_expectations_check(bypass)
end
+
+ test "sends email on failed directory sync", %{account: account} do
+ actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
+ _identity = Fixtures.Auth.create_identity(account: account, actor: actor)
+
+ error_message =
+ "Admin SDK API has not been used in project XXXX before or it is disabled. " <>
+ "Enable it by visiting https://console.developers.google.com/apis/api/admin.googleapis.com/overview?project=XXXX " <>
+ "then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry."
+
+ response = %{
+ "error" => %{
+ "code" => 403,
+ "message" => error_message,
+ "errors" => [
+ %{
+ "message" => error_message,
+ "domain" => "usageLimits",
+ "reason" => "accessNotConfigured",
+ "extendedHelp" => "https://console.developers.google.com"
+ }
+ ],
+ "status" => "PERMISSION_DENIED",
+ "details" => [
+ %{
+ "@type" => "type.googleapis.com/google.rpc.Help",
+ "links" => [
+ %{
+ "description" => "Google developers console API activation",
+ "url" =>
+ "https://console.developers.google.com/apis/api/admin.googleapis.com/overview?project=100421656358"
+ }
+ ]
+ },
+ %{
+ "@type" => "type.googleapis.com/google.rpc.ErrorInfo",
+ "reason" => "SERVICE_DISABLED",
+ "domain" => "googleapis.com",
+ "metadata" => %{
+ "service" => "admin.googleapis.com",
+ "consumer" => "projects/100421656358"
+ }
+ }
+ ]
+ }
+ }
+
+ bypass = Bypass.open()
+ GoogleWorkspaceDirectory.override_endpoint_url("http://localhost:#{bypass.port}/")
+
+ for path <- [
+ "/admin/directory/v1/users",
+ "/admin/directory/v1/customer/my_customer/orgunits",
+ "/admin/directory/v1/groups"
+ ] do
+ Bypass.stub(bypass, "GET", path, fn conn ->
+ Plug.Conn.send_resp(conn, 403, Jason.encode!(response))
+ end)
+ end
+
+ {:ok, pid} = Task.Supervisor.start_link()
+ assert execute(%{task_supervisor: pid}) == :ok
+
+ assert_email_sent(fn email ->
+ assert email.subject == "Firezone Identity Provider Sync Error"
+ assert email.text_body =~ "failed to sync 1 times"
+ end)
+
+ cancel_bypass_expectations_check(bypass)
+ end
end
end
diff --git a/elixir/apps/domain/test/domain/auth/adapters/jumpcloud/jobs/sync_directory_test.exs b/elixir/apps/domain/test/domain/auth/adapters/jumpcloud/jobs/sync_directory_test.exs
index 6d8dbf4c1..bbc30b437 100644
--- a/elixir/apps/domain/test/domain/auth/adapters/jumpcloud/jobs/sync_directory_test.exs
+++ b/elixir/apps/domain/test/domain/auth/adapters/jumpcloud/jobs/sync_directory_test.exs
@@ -561,5 +561,36 @@ defmodule Domain.Auth.Adapters.JumpCloud.Jobs.SyncDirectoryTest do
cancel_bypass_expectations_check(bypass)
end
+
+ test "sends email on failed directory sync", %{account: account} do
+ actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
+ _identity = Fixtures.Auth.create_identity(account: account, actor: actor)
+
+ bypass = Bypass.open()
+
+ WorkOSDirectory.override_base_url("http://localhost:#{bypass.port}")
+
+ for path <- [
+ "/directories",
+ "/directory_users",
+ "/directory_groups"
+ ] do
+ Bypass.stub(bypass, "GET", path, fn conn ->
+ conn
+ |> Plug.Conn.prepend_resp_headers([{"content-type", "application/json"}])
+ |> Plug.Conn.send_resp(500, Jason.encode!(%{}))
+ end)
+ end
+
+ {:ok, pid} = Task.Supervisor.start_link()
+ assert execute(%{task_supervisor: pid}) == :ok
+
+ assert_email_sent(fn email ->
+ assert email.subject == "Firezone Identity Provider Sync Error"
+ assert email.text_body =~ "failed to sync 1 times"
+ end)
+
+ cancel_bypass_expectations_check(bypass)
+ end
end
end
diff --git a/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/sync_directory_test.exs b/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/sync_directory_test.exs
index 4a02e4583..6234010fd 100644
--- a/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/sync_directory_test.exs
+++ b/elixir/apps/domain/test/domain/auth/adapters/microsoft_entra/jobs/sync_directory_test.exs
@@ -483,5 +483,32 @@ defmodule Domain.Auth.Adapters.MicrosoftEntra.Jobs.SyncDirectoryTest do
cancel_bypass_expectations_check(bypass)
end
+
+ test "sends email on failed directory sync", %{account: account} do
+ bypass = Bypass.open()
+ MicrosoftEntraDirectory.override_endpoint_url("http://localhost:#{bypass.port}/")
+
+ actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
+ _identity = Fixtures.Auth.create_identity(account: account, actor: actor)
+
+ for path <- [
+ "v1.0/users",
+ "v1.0/groups"
+ ] do
+ Bypass.stub(bypass, "GET", path, fn conn ->
+ Plug.Conn.send_resp(conn, 500, "")
+ end)
+ end
+
+ {:ok, pid} = Task.Supervisor.start_link()
+ assert execute(%{task_supervisor: pid}) == :ok
+
+ assert_email_sent(fn email ->
+ assert email.subject == "Firezone Identity Provider Sync Error"
+ assert email.text_body =~ "failed to sync 1 times"
+ end)
+
+ cancel_bypass_expectations_check(bypass)
+ end
end
end
diff --git a/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory.exs b/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory_test.exs
similarity index 93%
rename from elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory.exs
rename to elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory_test.exs
index 53d886092..d7b0a3ba3 100644
--- a/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory.exs
+++ b/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory_test.exs
@@ -33,8 +33,6 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do
end
test "syncs IdP data", %{provider: provider, bypass: bypass} do
- # bypass = Bypass.open(port: bypass.port)
-
groups = [
%{
"id" => "GROUP_DEVOPS_ID",
@@ -243,7 +241,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do
OktaDirectory.mock_group_members_list_endpoint(bypass, group["id"], members)
end)
- assert execute(%{}) == :ok
+ {:ok, pid} = Task.Supervisor.start_link()
+ assert execute(%{task_supervisor: pid}) == :ok
groups = Actors.Group |> Repo.all()
assert length(groups) == 2
@@ -287,7 +286,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do
test "does not crash on endpoint errors", %{bypass: bypass} do
Bypass.down(bypass)
- assert execute(%{}) == :ok
+ {:ok, pid} = Task.Supervisor.start_link()
+ assert execute(%{task_supervisor: pid}) == :ok
assert Repo.aggregate(Actors.Group, :count) == 0
end
@@ -337,7 +337,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do
OktaDirectory.mock_groups_list_endpoint(bypass, [])
OktaDirectory.mock_users_list_endpoint(bypass, users)
- assert execute(%{}) == :ok
+ {:ok, pid} = Task.Supervisor.start_link()
+ assert execute(%{task_supervisor: pid}) == :ok
assert updated_identity =
Repo.get(Domain.Auth.Identity, identity.id)
@@ -666,7 +667,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do
one_member
)
- assert execute(%{}) == :ok
+ {:ok, pid} = Task.Supervisor.start_link()
+ assert execute(%{task_supervisor: pid}) == :ok
assert updated_group = Repo.get(Domain.Actors.Group, group.id)
assert updated_group.name == "Group:Engineering"
@@ -758,7 +760,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do
end)
end
- assert execute(%{}) == :ok
+ {:ok, pid} = Task.Supervisor.start_link()
+ assert execute(%{task_supervisor: pid}) == :ok
assert updated_provider = Repo.get(Domain.Auth.Provider, provider.id)
refute updated_provider.last_synced_at
@@ -774,7 +777,8 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do
end)
end
- assert execute(%{}) == :ok
+ {:ok, pid} = Task.Supervisor.start_link()
+ assert execute(%{task_supervisor: pid}) == :ok
assert updated_provider = Repo.get(Domain.Auth.Provider, provider.id)
refute updated_provider.last_synced_at
@@ -783,5 +787,40 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do
cancel_bypass_expectations_check(bypass)
end
+
+ test "sends email on failed directory sync", %{
+ account: account,
+ bypass: bypass
+ } do
+ actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
+ _identity = Fixtures.Auth.create_identity(account: account, actor: actor)
+
+ response = %{
+ "errorCode" => "E0000011",
+ "errorSummary" => "Invalid token provided",
+ "errorLink" => "E0000011",
+ "errorId" => "sampleU-5P2FZVslkYBMP_Rsq",
+ "errorCauses" => []
+ }
+
+ for path <- [
+ "api/v1/users",
+ "api/v1/groups"
+ ] do
+ Bypass.stub(bypass, "GET", path, fn conn ->
+ Plug.Conn.send_resp(conn, 401, Jason.encode!(response))
+ end)
+ end
+
+ {:ok, pid} = Task.Supervisor.start_link()
+ assert execute(%{task_supervisor: pid}) == :ok
+
+ assert_email_sent(fn email ->
+ assert email.subject == "Firezone Identity Provider Sync Error"
+ assert email.text_body =~ "failed to sync 1 times"
+ end)
+
+ cancel_bypass_expectations_check(bypass)
+ end
end
end
diff --git a/elixir/apps/web/test/web/mailer/auth_email_test.exs b/elixir/apps/domain/test/domain/mailer/auth_email_test.exs
similarity index 93%
rename from elixir/apps/web/test/web/mailer/auth_email_test.exs
rename to elixir/apps/domain/test/domain/mailer/auth_email_test.exs
index 6152d2cda..32ccb8074 100644
--- a/elixir/apps/web/test/web/mailer/auth_email_test.exs
+++ b/elixir/apps/domain/test/domain/mailer/auth_email_test.exs
@@ -1,6 +1,6 @@
-defmodule Web.Mailer.AuthEmailTest do
- use Web.ConnCase, async: true
- import Web.Mailer.AuthEmail
+defmodule Domain.Mailer.AuthEmailTest do
+ use Domain.DataCase, async: true
+ import Domain.Mailer.AuthEmail
setup do
Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
diff --git a/elixir/apps/web/test/web/mailer/rate_limiter_test.exs b/elixir/apps/domain/test/domain/mailer/rate_limiter_test.exs
similarity index 97%
rename from elixir/apps/web/test/web/mailer/rate_limiter_test.exs
rename to elixir/apps/domain/test/domain/mailer/rate_limiter_test.exs
index cc8f28c25..31593012f 100644
--- a/elixir/apps/web/test/web/mailer/rate_limiter_test.exs
+++ b/elixir/apps/domain/test/domain/mailer/rate_limiter_test.exs
@@ -1,6 +1,6 @@
-defmodule Web.Mailer.RateLimiterTest do
+defmodule Domain.Mailer.RateLimiterTest do
use ExUnit.Case, async: true
- import Web.Mailer.RateLimiter
+ import Domain.Mailer.RateLimiter
describe "init/1" do
test "creates a ETS table" do
diff --git a/elixir/apps/domain/test/domain/mailer/sync_error_email_test.exs b/elixir/apps/domain/test/domain/mailer/sync_error_email_test.exs
new file mode 100644
index 000000000..0cbad69fe
--- /dev/null
+++ b/elixir/apps/domain/test/domain/mailer/sync_error_email_test.exs
@@ -0,0 +1,37 @@
+defmodule Domain.Mailer.SyncErrorEmailTest do
+ use Domain.DataCase, async: true
+ import Domain.Mailer.SyncEmail
+
+ setup do
+ account = Fixtures.Accounts.create_account()
+ {provider, _bypass} = Fixtures.Auth.start_and_create_okta_provider(account: account)
+
+ %{
+ account: account,
+ provider: provider
+ }
+ end
+
+ describe "sync_error_email/2" do
+ test "should contain sync error info", %{provider: provider} do
+ admin_email = "admin@foo.local"
+ expected_msg = "403 - Forbidden"
+
+ provider =
+ provider
+ |> Domain.Repo.preload(:account)
+ |> set_provider_failure("Error while syncing")
+ |> set_provider_failure(expected_msg)
+
+ email_body = sync_error_email(provider, admin_email)
+
+ assert email_body.text_body =~ "2 times"
+ assert email_body.text_body =~ expected_msg
+ end
+ end
+
+ defp set_provider_failure(provider, message) do
+ Domain.Auth.Provider.Changeset.sync_failed(provider, message)
+ |> Domain.Repo.update!()
+ end
+end
diff --git a/elixir/apps/web/test/web/mailer_test.exs b/elixir/apps/domain/test/domain/mailer_test.exs
similarity index 93%
rename from elixir/apps/web/test/web/mailer_test.exs
rename to elixir/apps/domain/test/domain/mailer_test.exs
index 6c118370f..dfbec4d71 100644
--- a/elixir/apps/web/test/web/mailer_test.exs
+++ b/elixir/apps/domain/test/domain/mailer_test.exs
@@ -1,6 +1,6 @@
-defmodule Web.MailerTest do
+defmodule Domain.MailerTest do
use ExUnit.Case, async: true
- import Web.Mailer
+ import Domain.Mailer
describe "deliver_with_rate_limit/2" do
test "delivers email with rate limit" do
diff --git a/elixir/apps/domain/test/support/data_case.ex b/elixir/apps/domain/test/support/data_case.ex
index 05af40bc9..37e16844b 100644
--- a/elixir/apps/domain/test/support/data_case.ex
+++ b/elixir/apps/domain/test/support/data_case.ex
@@ -20,6 +20,7 @@ defmodule Domain.DataCase do
quote do
import Ecto
import Ecto.Changeset
+ import Swoosh.TestAssertions
import Domain.DataCase
alias Domain.Repo
diff --git a/elixir/apps/web/test/support/mailer/mailer_test_adapter.ex b/elixir/apps/domain/test/support/mailer/mailer_test_adapter.ex
similarity index 89%
rename from elixir/apps/web/test/support/mailer/mailer_test_adapter.ex
rename to elixir/apps/domain/test/support/mailer/mailer_test_adapter.ex
index d39e756d7..6fd62db13 100644
--- a/elixir/apps/web/test/support/mailer/mailer_test_adapter.ex
+++ b/elixir/apps/domain/test/support/mailer/mailer_test_adapter.ex
@@ -1,4 +1,4 @@
-defmodule Web.Mailer.TestAdapter do
+defmodule Domain.Mailer.TestAdapter do
use Swoosh.Adapter
@impl true
diff --git a/elixir/apps/web/.formatter.exs b/elixir/apps/web/.formatter.exs
index fe4eec393..4978f4996 100644
--- a/elixir/apps/web/.formatter.exs
+++ b/elixir/apps/web/.formatter.exs
@@ -1,7 +1,10 @@
[
import_deps: [:phoenix, :phoenix_live_view],
plugins: [Phoenix.LiveView.HTMLFormatter],
- inputs: ["*.{xml.heex,html.heex,ex,exs}", "{config,lib,test}/**/*.{xml.heex,html.heex,ex,exs}"],
+ inputs: [
+ "*.{xml.heex,html.heex,ex,exs}",
+ "{config,lib,test}/**/*.{xml.heex,html.heex,ex,exs}"
+ ],
locals_without_parens: [
assert_authenticated: 2,
assert_unauthenticated: 1,
diff --git a/elixir/apps/web/lib/web/application.ex b/elixir/apps/web/lib/web/application.ex
index ab2e3d77e..c0d740891 100644
--- a/elixir/apps/web/lib/web/application.ex
+++ b/elixir/apps/web/lib/web/application.ex
@@ -8,8 +8,6 @@ defmodule Web.Application do
_ = OpentelemetryPhoenix.setup(adapter: :cowboy2)
children = [
- Web.Mailer,
- Web.Mailer.RateLimiter,
Web.Endpoint
]
diff --git a/elixir/apps/web/lib/web/components/core_components.ex b/elixir/apps/web/lib/web/components/core_components.ex
index 3fdfb67b3..a9d6447b8 100644
--- a/elixir/apps/web/lib/web/components/core_components.ex
+++ b/elixir/apps/web/lib/web/components/core_components.ex
@@ -1044,16 +1044,11 @@ defmodule Web.CoreComponents do
end
def get_identity_email(identity) do
- provider_email(identity) || identity.provider_identifier
+ Domain.Auth.get_identity_email(identity)
end
def identity_has_email?(identity) do
- not is_nil(provider_email(identity)) or identity.provider.adapter == :email or
- identity.provider_identifier =~ "@"
- end
-
- defp provider_email(identity) do
- get_in(identity.provider_state, ["userinfo", "email"])
+ Domain.Auth.identity_has_email?(identity)
end
attr :account, :any, required: true
diff --git a/elixir/apps/web/lib/web/controllers/auth_controller.ex b/elixir/apps/web/lib/web/controllers/auth_controller.ex
index 1f6421c78..86371b567 100644
--- a/elixir/apps/web/lib/web/controllers/auth_controller.ex
+++ b/elixir/apps/web/lib/web/controllers/auth_controller.ex
@@ -159,14 +159,14 @@ defmodule Web.AuthController do
# attacks where you can trick user into logging in into an attacker account.
fragment = identity.provider_virtual_state.fragment
- Web.Mailer.AuthEmail.sign_in_link_email(
+ Domain.Mailer.AuthEmail.sign_in_link_email(
identity,
nonce,
conn.assigns.user_agent,
conn.remote_ip,
redirect_params
)
- |> Web.Mailer.deliver_with_rate_limit(
+ |> Domain.Mailer.deliver_with_rate_limit(
rate_limit_key: {:sign_in_link, identity.id},
rate_limit: 3,
rate_limit_interval: :timer.minutes(5)
@@ -207,7 +207,7 @@ defmodule Web.AuthController do
with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id),
{:ok, identity, encoded_fragment} <-
Domain.Auth.sign_in(provider, identity_id, nonce, secret, context) do
- :ok = Web.Mailer.RateLimiter.reset_rate_limit({:sign_in_link, identity.id})
+ :ok = Domain.Mailer.RateLimiter.reset_rate_limit({:sign_in_link, identity.id})
Web.Auth.signed_in(conn, provider, identity, context, encoded_fragment, redirect_params)
else
{:error, :not_found} ->
diff --git a/elixir/apps/web/lib/web/live/actors/show.ex b/elixir/apps/web/lib/web/live/actors/show.ex
index bbeb5ff1e..10a7cd10c 100644
--- a/elixir/apps/web/lib/web/live/actors/show.ex
+++ b/elixir/apps/web/lib/web/live/actors/show.ex
@@ -636,12 +636,12 @@ defmodule Web.Actors.Show do
def handle_event("send_welcome_email", %{"id" => id}, socket) do
{:ok, identity} = Auth.fetch_identity_by_id(id, socket.assigns.subject)
- Web.Mailer.AuthEmail.new_user_email(
+ Domain.Mailer.AuthEmail.new_user_email(
socket.assigns.account,
identity,
socket.assigns.subject
)
- |> Web.Mailer.deliver_with_rate_limit(
+ |> Domain.Mailer.deliver_with_rate_limit(
rate_limit: 3,
rate_limit_key: {:welcome_email, identity.id},
rate_limit_interval: :timer.minutes(3)
diff --git a/elixir/apps/web/lib/web/live/actors/users/new_identity.ex b/elixir/apps/web/lib/web/live/actors/users/new_identity.ex
index 4a65db196..9c1846baa 100644
--- a/elixir/apps/web/lib/web/live/actors/users/new_identity.ex
+++ b/elixir/apps/web/lib/web/live/actors/users/new_identity.ex
@@ -111,12 +111,12 @@ defmodule Web.Actors.Users.NewIdentity do
socket.assigns.subject
) do
if socket.assigns.provider.adapter == :email do
- Web.Mailer.AuthEmail.new_user_email(
+ Domain.Mailer.AuthEmail.new_user_email(
socket.assigns.account,
identity,
socket.assigns.subject
)
- |> Web.Mailer.deliver()
+ |> Domain.Mailer.deliver()
end
socket = push_navigate(socket, to: next_path(socket))
diff --git a/elixir/apps/web/lib/web/live/settings/api_clients/beta.ex b/elixir/apps/web/lib/web/live/settings/api_clients/beta.ex
index db2c2dd88..40292c526 100644
--- a/elixir/apps/web/lib/web/live/settings/api_clients/beta.ex
+++ b/elixir/apps/web/lib/web/live/settings/api_clients/beta.ex
@@ -62,11 +62,11 @@ defmodule Web.Settings.ApiClients.Beta do
end
def handle_event("request_access", _params, socket) do
- Web.Mailer.BetaEmail.rest_api_beta_email(
+ Domain.Mailer.BetaEmail.rest_api_beta_email(
socket.assigns.account,
socket.assigns.subject
)
- |> Web.Mailer.deliver()
+ |> Domain.Mailer.deliver()
socket =
socket
diff --git a/elixir/apps/web/lib/web/live/sign_up.ex b/elixir/apps/web/lib/web/live/sign_up.ex
index b9fc2968d..1d841e163 100644
--- a/elixir/apps/web/lib/web/live/sign_up.ex
+++ b/elixir/apps/web/lib/web/live/sign_up.ex
@@ -469,13 +469,13 @@ defmodule Web.SignUp do
|> Ecto.Multi.run(
:send_email,
fn _repo, %{account: account, identity: identity} ->
- Web.Mailer.AuthEmail.sign_up_link_email(
+ Domain.Mailer.AuthEmail.sign_up_link_email(
account,
identity,
socket.assigns.user_agent,
socket.assigns.real_ip
)
- |> Web.Mailer.deliver_with_rate_limit(
+ |> Domain.Mailer.deliver_with_rate_limit(
rate_limit_key: {:sign_up_link, String.downcase(identity.provider_identifier)},
rate_limit: 3,
rate_limit_interval: :timer.minutes(30)
diff --git a/elixir/apps/web/mix.exs b/elixir/apps/web/mix.exs
index ee57cbbc5..01201a91c 100644
--- a/elixir/apps/web/mix.exs
+++ b/elixir/apps/web/mix.exs
@@ -48,10 +48,7 @@ defmodule Web.MixProject do
{:gettext, "~> 0.20"},
{:remote_ip, "~> 1.0"},
- # CLDR and unit conversions
- {:ex_cldr_dates_times, "~> 2.13"},
- {:ex_cldr_numbers, "~> 2.31"},
- {:ex_cldr, "~> 2.40"},
+ # Unit conversions
{:tzdata, "~> 1.1"},
{:sizeable, "~> 1.0"},
@@ -65,11 +62,6 @@ defmodule Web.MixProject do
{:recon, "~> 2.5"},
{:observer_cli, "~> 1.7"},
- # Mailer deps
- {:multipart, "~> 0.4.0"},
- {:phoenix_swoosh, "~> 1.0"},
- {:gen_smtp, "~> 1.0"},
-
# Observability
{:opentelemetry_telemetry, "~> 1.1.1", override: true},
{:opentelemetry_cowboy, "~> 0.3"},
diff --git a/elixir/apps/web/test/web/controllers/auth_controller_test.exs b/elixir/apps/web/test/web/controllers/auth_controller_test.exs
index 262b46498..5ccdfa5df 100644
--- a/elixir/apps/web/test/web/controllers/auth_controller_test.exs
+++ b/elixir/apps/web/test/web/controllers/auth_controller_test.exs
@@ -779,7 +779,7 @@ defmodule Web.AuthControllerTest do
email_secret: email_secret
} do
key = {:sign_in_link, identity.id}
- Web.Mailer.RateLimiter.rate_limit(key, 3, 60_000, fn -> :ok end)
+ Domain.Mailer.RateLimiter.rate_limit(key, 3, 60_000, fn -> :ok end)
conn =
conn
@@ -791,7 +791,7 @@ defmodule Web.AuthControllerTest do
assert conn.assigns.flash == %{}
assert redirected_to(conn) == ~p"/#{account}/sites"
- refute :ets.tab2list(Web.Mailer.RateLimiter.ETS)
+ refute :ets.tab2list(Domain.Mailer.RateLimiter.ETS)
|> Enum.any?(fn {ets_key, _, _} -> ets_key == key end)
end
end
diff --git a/elixir/config/config.exs b/elixir/config/config.exs
index 7a3fe7e33..c7bb12943 100644
--- a/elixir/config/config.exs
+++ b/elixir/config/config.exs
@@ -100,6 +100,8 @@ config :domain, docker_registry: "us-east1-docker.pkg.dev/firezone-staging/firez
config :domain, outbound_email_adapter_configured?: false
+config :domain, web_external_url: "http://localhost:13000"
+
###############################
##### Web #####################
###############################
@@ -213,7 +215,7 @@ config :phoenix, :json_library, Jason
config :swoosh, :api_client, Swoosh.ApiClient.Finch
-config :web, Web.Mailer,
+config :domain, Domain.Mailer,
adapter: Domain.Mailer.NoopAdapter,
from_email: "test@firez.one"
diff --git a/elixir/config/dev.exs b/elixir/config/dev.exs
index 48391e01f..0b8499306 100644
--- a/elixir/config/dev.exs
+++ b/elixir/config/dev.exs
@@ -69,7 +69,7 @@ config :phoenix_live_reload, :dirs, [
config :web, Web.Plugs.SecureHeaders,
csp_policy: [
"default-src 'self' 'nonce-${nonce}' https://api-js.mixpanel.com",
- "img-src 'self' data: https://www.gravatar.com https://track.hubspot.com",
+ "img-src 'self' data: https://www.gravatar.com https://track.hubspot.com https://www.firezone.dev",
"style-src 'self' 'unsafe-inline'",
"script-src 'self' 'unsafe-inline' http://cdn.mxpnl.com http://*.hs-analytics.net https://cdn.tailwindcss.com/"
]
@@ -109,7 +109,7 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
-config :web, Web.Mailer, adapter: Swoosh.Adapters.Local
+config :domain, Domain.Mailer, adapter: Swoosh.Adapters.Local
config :workos, WorkOS.Client,
api_key: System.get_env("WORKOS_API_KEY"),
diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs
index 08f601623..15037e0a3 100644
--- a/elixir/config/runtime.exs
+++ b/elixir/config/runtime.exs
@@ -73,6 +73,8 @@ if config_env() == :prod do
config :domain, outbound_email_adapter_configured?: !!compile_config!(:outbound_email_adapter)
+ config :domain, web_external_url: compile_config!(:web_external_url)
+
# Enable background jobs only on dedicated nodes
config :domain, Domain.Tokens.Jobs.DeleteExpiredTokens,
enabled: compile_config!(:background_jobs_enabled)
@@ -221,8 +223,8 @@ if config_env() == :prod do
config :openid_connect,
finch_transport_opts: compile_config!(:http_client_ssl_opts)
- config :web,
- Web.Mailer,
+ config :domain,
+ Domain.Mailer,
[
adapter: compile_config!(:outbound_email_adapter),
from_email: compile_config!(:outbound_email_from)
diff --git a/elixir/config/test.exs b/elixir/config/test.exs
index cb3339e91..2a17e49e7 100644
--- a/elixir/config/test.exs
+++ b/elixir/config/test.exs
@@ -28,6 +28,8 @@ config :domain, Domain.GoogleCloudPlatform, service_account_email: "foo@iam.exam
config :domain, Domain.Telemetry.GoogleCloudMetricsReporter, project_id: "fz-test"
+config :domain, web_external_url: "http://localhost:13100"
+
###############################
##### Web #####################
###############################
@@ -57,7 +59,7 @@ config :api, API.Endpoint,
###############################
##### Third-party configs #####
###############################
-config :web, Web.Mailer, adapter: Web.Mailer.TestAdapter
+config :domain, Domain.Mailer, adapter: Domain.Mailer.TestAdapter
config :logger, level: :warning