mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
Add ability to email new user after creation (#2957)
Why: * When a new user and/or identity is created using the Email provider, there is currently no way to notify the new user/identity automatically. With this commit an email will now be sent to the newly added user/identity upon successful creation. This will only be done for identities created with the 'Email' provider. <img width="621" alt="new_user_email" src="https://github.com/firezone/firezone/assets/2646332/2e50baf0-34cf-4615-b7f9-30500aa58920"> --------- Signed-off-by: Brian Manifold <bmanifold@users.noreply.github.com> Co-authored-by: Andrew Dryga <andrew@dryga.com>
This commit is contained in:
@@ -345,7 +345,8 @@ defmodule Web.CoreComponents do
|
||||
class={[
|
||||
"p-4 text-sm flash-#{@kind}",
|
||||
@kind == :success && "text-green-800 bg-green-50",
|
||||
@kind == :info && "text-yellow-800 bg-yellow-50",
|
||||
@kind == :info && "text-blue-800 bg-blue-50",
|
||||
@kind == :warning && "text-yellow-800 bg-yellow-50",
|
||||
@kind == :error && "text-red-800 bg-red-50",
|
||||
@style != "wide" && "mb-4 rounded"
|
||||
]}
|
||||
@@ -937,7 +938,15 @@ defmodule Web.CoreComponents do
|
||||
end
|
||||
|
||||
def get_identity_email(identity) do
|
||||
get_in(identity.provider_state, ["userinfo", "email"]) || identity.provider_identifier
|
||||
provider_email(identity) || identity.provider_identifier
|
||||
end
|
||||
|
||||
def identity_has_email?(identity) do
|
||||
not is_nil(provider_email(identity)) || identity.provider.adapter == :email
|
||||
end
|
||||
|
||||
defp provider_email(identity) do
|
||||
get_in(identity.provider_state, ["userinfo", "email"])
|
||||
end
|
||||
|
||||
attr :account, :any, required: true
|
||||
|
||||
@@ -32,7 +32,7 @@ defmodule Web.PageComponents do
|
||||
</p>
|
||||
|
||||
<section :for={content <- @content} class="section-body">
|
||||
<div :if={Map.get(content, :flash)}>
|
||||
<div :if={Map.get(content, :flash)} class="mb-4">
|
||||
<.flash kind={:info} flash={Map.get(content, :flash)} style="wide" />
|
||||
<.flash kind={:error} flash={Map.get(content, :flash)} style="wide" />
|
||||
</div>
|
||||
|
||||
@@ -123,6 +123,18 @@ defmodule Web.Actors.Show do
|
||||
<:col :let={identity} label="LAST SIGNED IN" sortable="false">
|
||||
<.relative_datetime datetime={identity.last_seen_at} />
|
||||
</:col>
|
||||
<:action :let={identity}>
|
||||
<button
|
||||
:if={identity_has_email?(identity)}
|
||||
phx-click="send_welcome_email"
|
||||
phx-value-id={identity.id}
|
||||
class={[
|
||||
"block w-full py-2 px-4 hover:bg-neutral-100"
|
||||
]}
|
||||
>
|
||||
Send Welcome Email
|
||||
</button>
|
||||
</:action>
|
||||
<:action :let={identity}>
|
||||
<button
|
||||
:if={identity.created_by != :provider}
|
||||
@@ -353,6 +365,24 @@ defmodule Web.Actors.Show do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("send_welcome_email", %{"id" => id}, socket) do
|
||||
{:ok, identity} = Auth.fetch_identity_by_id(id, socket.assigns.subject)
|
||||
|
||||
{:ok, _} =
|
||||
Web.Mailer.AuthEmail.new_user_email(
|
||||
socket.assigns.account,
|
||||
identity,
|
||||
socket.assigns.subject
|
||||
)
|
||||
|> Web.Mailer.deliver()
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:info, "Welcome email sent to #{identity.provider_identifier}")
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp last_seen_at(identities) do
|
||||
identities
|
||||
|> Enum.reject(&is_nil(&1.last_seen_at))
|
||||
|
||||
@@ -95,13 +95,22 @@ defmodule Web.Actors.Users.NewIdentity do
|
||||
end
|
||||
|
||||
def handle_event("submit", %{"identity" => attrs}, socket) do
|
||||
with {:ok, _identity} <-
|
||||
with {:ok, identity} <-
|
||||
Auth.create_identity(
|
||||
socket.assigns.actor,
|
||||
socket.assigns.provider,
|
||||
attrs,
|
||||
socket.assigns.subject
|
||||
) do
|
||||
if socket.assigns.provider.adapter == :email do
|
||||
Web.Mailer.AuthEmail.new_user_email(
|
||||
socket.assigns.account,
|
||||
identity,
|
||||
socket.assigns.subject
|
||||
)
|
||||
|> Web.Mailer.deliver()
|
||||
end
|
||||
|
||||
socket =
|
||||
push_navigate(socket, to: ~p"/#{socket.assigns.account}/actors/#{socket.assigns.actor}")
|
||||
|
||||
|
||||
@@ -59,4 +59,19 @@ defmodule Web.Mailer.AuthEmail do
|
||||
remote_ip: "#{:inet.ntoa(remote_ip)}"
|
||||
)
|
||||
end
|
||||
|
||||
def new_user_email(
|
||||
%Domain.Accounts.Account{} = account,
|
||||
%Domain.Auth.Identity{} = identity,
|
||||
%Domain.Auth.Subject{} = subject
|
||||
) do
|
||||
default_email()
|
||||
|> subject("Welcome to Firezone")
|
||||
|> to(get_identity_email(identity))
|
||||
|> render_body(__MODULE__, :new_user,
|
||||
account: account,
|
||||
identity: identity,
|
||||
subject: subject
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
172
elixir/apps/web/lib/web/mailer/auth_email/new_user.html.heex
Normal file
172
elixir/apps/web/lib/web/mailer/auth_email/new_user.html.heex
Normal file
@@ -0,0 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="supported-color-schemes" content="light dark" />
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<style>
|
||||
td,th,div,p,a,h1,h2,h3,h4,h5,h6 {font-family: "Segoe UI", sans-serif; mso-line-height-rule: exactly;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<title>Welcome to Firezone</title>
|
||||
<style>
|
||||
.hover-important-text-decoration-underline:hover {
|
||||
text-decoration: underline !important
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.sm-my-8 {
|
||||
margin-top: 32px !important;
|
||||
margin-bottom: 32px !important
|
||||
}
|
||||
.sm-px-4 {
|
||||
padding-left: 16px !important;
|
||||
padding-right: 16px !important
|
||||
}
|
||||
.sm-px-6 {
|
||||
padding-left: 24px !important;
|
||||
padding-right: 24px !important
|
||||
}
|
||||
.sm-leading-8 {
|
||||
line-height: 32px !important
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; width: 100%; background-color: #f6f6f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word">
|
||||
<div role="article" aria-roledescription="email" aria-label="Welcome to Firezone" lang="en">
|
||||
<div
|
||||
class="sm-px-4"
|
||||
style="background-color: #f6f6f6; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif"
|
||||
>
|
||||
<table align="center" cellpadding="0" cellspacing="0" role="none">
|
||||
<tr>
|
||||
<td style="width: 552px; max-width: 100%">
|
||||
<div
|
||||
class="sm-my-8"
|
||||
style="margin-top: 48px; margin-bottom: 48px; text-align: center"
|
||||
>
|
||||
<a href="https://firezone.dev">
|
||||
<div>
|
||||
<img
|
||||
src="https://www.firezone.dev/images/logo-lockup.png"
|
||||
width="250"
|
||||
alt="Firezone logo"
|
||||
style="max-width: 100%; vertical-align: middle; line-height: 1"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="none">
|
||||
<tr>
|
||||
<td
|
||||
class="sm-px-6"
|
||||
style="border-radius: 4px; background-color: #ffffff; padding: 48px; font-size: 16px; color: #4f4f4f; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)"
|
||||
>
|
||||
<h1
|
||||
class="sm-leading-8"
|
||||
style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #000000"
|
||||
>
|
||||
Welcome to Firezone!
|
||||
</h1>
|
||||
<p style="margin: 0; line-height: 24px">
|
||||
<%= @subject.actor.name %> invited you to the following Firezone Account:<br />
|
||||
</p>
|
||||
<p>
|
||||
<b>"<%= @account.name %>"</b>
|
||||
</p>
|
||||
<div role="separator" style="line-height: 16px">‍</div>
|
||||
To start accessing your resources, simply install one of the Firezone clients:
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://www.firezone.dev/kb/user-guides/apple-client">
|
||||
Apple Client (macOS/iOS)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.firezone.dev/kb/user-guides/windows-client">
|
||||
Windows Client
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.firezone.dev/kb/user-guides/android-client">
|
||||
Android / ChromeOS Client
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.firezone.dev/kb/user-guides/linux-client">
|
||||
Linux Client
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
After installing the client, click "Sign In" and when prompted for an account name use the following:
|
||||
<p style="border-radius: 4px; border: 1px solid #e7e7e7; padding: 8px; text-align: center; font-size: 20px">
|
||||
<code><%= @account.slug %></code>
|
||||
</p>
|
||||
<p></p>
|
||||
<div
|
||||
role="separator"
|
||||
style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0"
|
||||
>
|
||||
‍
|
||||
</div>
|
||||
<p style="margin: 0;">
|
||||
If you feel this message has been sent to you by mistake, you can safely ignore this email.
|
||||
<br />
|
||||
<br /> Thanks, <br />The Firezone Team
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr role="separator">
|
||||
<td style="line-height: 48px">‍</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
|
||||
<p style="margin: 0; font-style: italic">
|
||||
Blazing-fast alternative to legacy VPNs
|
||||
</p>
|
||||
<p style="cursor: default">
|
||||
<a
|
||||
href="https://www.firezone.dev/kb"
|
||||
class="hover-important-text-decoration-underline"
|
||||
style="color: #37007f; text-decoration: none"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
•
|
||||
<a
|
||||
href="https://github.com/firezone"
|
||||
class="hover-important-text-decoration-underline"
|
||||
style="color: #37007f; text-decoration: none;"
|
||||
>
|
||||
Github
|
||||
</a>
|
||||
•
|
||||
<a
|
||||
href="https://x.com/firezonehq"
|
||||
class="hover-important-text-decoration-underline"
|
||||
style="color: #37007f; text-decoration: none;"
|
||||
>
|
||||
X
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
22
elixir/apps/web/lib/web/mailer/auth_email/new_user.text.heex
Normal file
22
elixir/apps/web/lib/web/mailer/auth_email/new_user.text.heex
Normal file
@@ -0,0 +1,22 @@
|
||||
Welcome to Firezone!
|
||||
|
||||
<%= "\n" %><%= @subject.actor.name %> invited you to the following Firezone Account:
|
||||
|
||||
<%= @account.name %><%= "\n" %>
|
||||
|
||||
To get started accessing your resources, you will need to install one of the Firezone clients:
|
||||
|
||||
https://www.firezone.dev/kb/user-guides/apple-client
|
||||
https://www.firezone.dev/kb/user-guides/windows-client
|
||||
https://www.firezone.dev/kb/user-guides/android-client
|
||||
https://www.firezone.dev/kb/user-guides/linux-client
|
||||
|
||||
|
||||
After installing the client, click "Sign In" and when prompted for an account name use the following:
|
||||
|
||||
<%= @account.slug %><%= "\n" %>
|
||||
|
||||
If you feel this message has been sent to you by mistake, you can safely ignore this email.
|
||||
|
||||
Thanks,
|
||||
The Firezone Team
|
||||
@@ -338,6 +338,90 @@ defmodule Web.Live.Actors.ShowTest do
|
||||
)
|
||||
end
|
||||
|
||||
test "allows sending welcome email", %{
|
||||
account: account,
|
||||
actor: actor,
|
||||
identity: admin_identity,
|
||||
conn: conn
|
||||
} do
|
||||
Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
|
||||
email_provider = Fixtures.Auth.create_email_provider(account: account)
|
||||
|
||||
email_identity =
|
||||
Fixtures.Auth.create_identity(account: account, actor: actor, provider: email_provider)
|
||||
|> Ecto.Changeset.change(
|
||||
created_by: :identity,
|
||||
created_by_identity_id: admin_identity.id
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(admin_identity)
|
||||
|> live(~p"/#{account}/actors/#{actor}")
|
||||
|
||||
assert lv
|
||||
|> element("#identity-#{email_identity.id} button", "Send Welcome Email")
|
||||
|> render_click()
|
||||
|> Floki.find(".flash-info")
|
||||
|> element_to_text() =~ "Welcome email sent to #{email_identity.provider_identifier}"
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
assert email.subject == "Welcome to Firezone"
|
||||
assert email.text_body =~ account.slug
|
||||
end)
|
||||
end
|
||||
|
||||
test "shows email button for identities with email", %{
|
||||
account: account,
|
||||
actor: actor,
|
||||
identity: admin_identity,
|
||||
conn: conn
|
||||
} do
|
||||
Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
|
||||
email_provider = Fixtures.Auth.create_email_provider(account: account)
|
||||
|
||||
{google_provider, _bypass} =
|
||||
Fixtures.Auth.start_and_create_google_workspace_provider(account: account)
|
||||
|
||||
google_identity =
|
||||
Fixtures.Auth.create_identity(
|
||||
account: account,
|
||||
actor: actor,
|
||||
provider: google_provider,
|
||||
provider_state: %{
|
||||
"userinfo" => %{"email" => Fixtures.Auth.email()}
|
||||
}
|
||||
)
|
||||
|
||||
oidc_identity = Fixtures.Auth.create_identity(account: account, actor: actor)
|
||||
|
||||
email_identity =
|
||||
Fixtures.Auth.create_identity(account: account, actor: actor, provider: email_provider)
|
||||
|> Ecto.Changeset.change(
|
||||
created_by: :identity,
|
||||
created_by_identity_id: admin_identity.id
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(admin_identity)
|
||||
|> live(~p"/#{account}/actors/#{actor}")
|
||||
|
||||
assert lv
|
||||
|> element("#identity-#{email_identity.id} button", "Send Welcome Email")
|
||||
|> has_element?()
|
||||
|
||||
assert lv
|
||||
|> element("#identity-#{google_identity.id} button", "Send Welcome Email")
|
||||
|> has_element?()
|
||||
|
||||
refute lv
|
||||
|> element("#identity-#{oidc_identity.id} button", "Send Welcome Email")
|
||||
|> has_element?()
|
||||
end
|
||||
|
||||
test "allows deleting identities", %{
|
||||
account: account,
|
||||
actor: actor,
|
||||
|
||||
@@ -186,5 +186,9 @@ defmodule Web.Live.Actors.User.NewIdentityTest do
|
||||
Repo.get_by(Domain.Auth.Identity, provider_identifier: attrs.provider_identifier)
|
||||
|
||||
assert_redirect(lv, ~p"/#{account}/actors/#{identity.actor_id}")
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
assert email.text_body =~ account.slug
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
54
elixir/apps/web/test/web/mailer/auth_mail.exs
Normal file
54
elixir/apps/web/test/web/mailer/auth_mail.exs
Normal file
@@ -0,0 +1,54 @@
|
||||
defmodule Web.Mailer.AuthEmailTest do
|
||||
use Web.ConnCase, async: true
|
||||
import Web.Mailer.AuthEmail
|
||||
|
||||
setup do
|
||||
Domain.Config.put_env_override(:outbound_email_adapter_configured?, true)
|
||||
account = Fixtures.Accounts.create_account()
|
||||
provider = Fixtures.Auth.create_email_provider(account: account)
|
||||
|
||||
admin_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
|
||||
|
||||
admin_identity =
|
||||
Fixtures.Auth.create_identity(account: account, provider: provider, actor: admin_actor)
|
||||
|
||||
client_actor = Fixtures.Actors.create_actor(type: :account_user, account: account)
|
||||
|
||||
client_identity =
|
||||
Fixtures.Auth.create_identity(account: account, provider: provider, actor: client_actor)
|
||||
|
||||
%{
|
||||
account: account,
|
||||
provider: provider,
|
||||
admin_actor: admin_actor,
|
||||
admin_identity: admin_identity,
|
||||
client_actor: client_actor,
|
||||
client_identity: client_identity
|
||||
}
|
||||
end
|
||||
|
||||
describe "new_user_email/3" do
|
||||
test "should contain relevant account and user info", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
admin_actor: admin_actor,
|
||||
admin_identity: admin_identity,
|
||||
client_identity: client_identity
|
||||
} do
|
||||
admin_subject =
|
||||
Fixtures.Auth.create_subject(
|
||||
account: account,
|
||||
provider: provider,
|
||||
identity: admin_identity,
|
||||
actor: admin_actor
|
||||
)
|
||||
|
||||
email_body = new_user_email(account, client_identity, admin_subject)
|
||||
|
||||
assert email_body.text_body =~ "Welcome to Firezone!"
|
||||
assert email_body.text_body =~ "#{admin_actor.name} invited you"
|
||||
assert email_body.text_body =~ account.name
|
||||
assert email_body.text_body =~ account.slug
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user