Send welcome emails (#2618)

And block colliding account slugs from being created.

Closes #2599
This commit is contained in:
Andrew Dryga
2023-11-09 11:41:58 -06:00
committed by GitHub
parent a7701c07de
commit 4eb2c91633
9 changed files with 160 additions and 12 deletions

View File

@@ -30,6 +30,16 @@ defmodule Domain.Accounts.Account.Changeset do
|> validate_format(:slug, ~r/^[a-zA-Z0-9_]+$/,
message: "can only contain letters, numbers, and underscores"
)
|> validate_exclusion(:slug, [
"sign_up",
"sign_in",
"sign_out",
"account",
"admin",
"system",
"me",
"you"
])
|> validate_change(:slug, fn field, slug ->
if valid_uuid?(slug) do
[{field, "cannot be a valid UUID"}]

View File

@@ -428,7 +428,7 @@ defmodule Web.Auth do
end)
end
defp real_ip(socket) do
def real_ip(socket) do
peer_data = Phoenix.LiveView.get_connect_info(socket, :peer_data)
x_headers = Phoenix.LiveView.get_connect_info(socket, :x_headers)

View File

@@ -31,6 +31,9 @@ defmodule Web.SignUp do
end
def mount(_params, _session, socket) do
user_agent = Phoenix.LiveView.get_connect_info(socket, :user_agent)
real_ip = Web.Auth.real_ip(socket)
changeset =
Registration.changeset(%Registration{}, %{
account: %{slug: "placeholder"},
@@ -41,6 +44,8 @@ defmodule Web.SignUp do
assign(socket,
form: to_form(changeset),
account: nil,
user_agent: user_agent,
real_ip: real_ip,
sign_up_enabled?: Config.sign_up_enabled?()
)
@@ -96,7 +101,7 @@ defmodule Web.SignUp do
~H"""
<div class="space-y-6">
<div class="text-center text-gray-900 dark:text-white">
Your account has been created!
Your account has been created! Please check your email for a sign in link.
</div>
<div class="text-center">
<div class="px-12">
@@ -281,8 +286,18 @@ defmodule Web.SignUp do
)
case Domain.Repo.transaction(multi) do
{:ok, result} ->
socket = assign(socket, account: result.account)
{:ok, %{account: account, identity: identity}} ->
{:ok, _} =
Web.Mailer.AuthEmail.sign_up_link_email(
account,
identity,
identity.provider_virtual_state.sign_in_token,
socket.assigns.user_agent,
socket.assigns.real_ip
)
|> Web.Mailer.deliver()
socket = assign(socket, account: account)
{:noreply, socket}
{:error, :account, err_changeset, _effects_so_far} ->

View File

@@ -6,6 +6,43 @@ defmodule Web.Mailer.AuthEmail do
embed_templates "auth_email/*.html", suffix: "_html"
embed_templates "auth_email/*.text", suffix: "_text"
def sign_up_link_email(
%Domain.Accounts.Account{} = account,
%Domain.Auth.Identity{} = identity,
email_secret,
user_agent,
remote_ip
) do
params =
%{
identity_id: identity.id,
secret: email_secret
}
sign_in_url =
url(
~p"/#{account}/sign_in/providers/#{identity.provider_id}/verify_sign_in_token?#{params}"
)
sign_in_form_url = url(~p"/#{account}")
default_email()
|> subject("Welcome to Firezone")
|> to(identity.provider_identifier)
|> render_body(__MODULE__, :sign_up_link,
account: account,
sign_in_token_created_at:
Cldr.DateTime.to_string!(identity.provider_state["sign_in_token_created_at"], Web.CLDR,
format: :short
) <> " UTC",
secret: email_secret,
sign_in_url: sign_in_url,
sign_in_form_url: sign_in_form_url,
user_agent: user_agent,
remote_ip: "#{:inet.ntoa(remote_ip)}"
)
end
def sign_in_link_email(
%Domain.Auth.Identity{} = identity,
email_secret,
@@ -19,7 +56,7 @@ defmodule Web.Mailer.AuthEmail do
secret: email_secret
})
sign_in_link =
sign_in_url =
url(
~p"/#{identity.account}/sign_in/providers/#{identity.provider_id}/verify_sign_in_token?#{params}"
)
@@ -35,7 +72,7 @@ defmodule Web.Mailer.AuthEmail do
format: :short
) <> " UTC",
secret: email_secret,
link: sign_in_link,
sign_in_url: sign_in_url,
user_agent: user_agent,
remote_ip: "#{:inet.ntoa(remote_ip)}"
)

View File

@@ -6,13 +6,13 @@
<div :if={is_nil(@client_platform)}>
<p>
Here is the <a href={@link} target="_blank">magic sign-in link</a>
Here is the <a href={@sign_in_url} target="_blank">magic sign-in link</a>
you requested to sign in to <b>"<%= @account.name %>"</b>.
It is valid for 15 minutes.
</p>
<small>
If the link didn't work, please copy this link and open it in your browser. <%= @link %>
If the link didn't work, please copy this link and open it in your browser: <%= @sign_in_url %>
</small>
</div>

View File

@@ -2,7 +2,7 @@ Dear Firezone user,
<%= if is_nil(@client_platform) do %>
Here is the magic sign-in link you requested to sign in to "<%= @account.name %>":
<%= @link %>
<%= @sign_in_url %>
Please copy this link and open it in your browser. It is valid for 15 minutes.
<% else %>

View File

@@ -0,0 +1,36 @@
<h3>Thank you for signing up for Firezone!</h3>
<div>
<p>
Here is the <a href={@sign_in_url} target="_blank">sign-in link</a>
to access your account <b>"<%= @account.name %>"</b>.
It is valid for 15 minutes.
</p>
<small>
If the link didn't work, please copy this link and open it in your browser. <%= @sign_in_url %>
</small>
</div>
<div>
<p>In future you can always access the sign in form at the following URL:</p>
<small>
<%= @sign_in_form_url %>
</small>
</div>
<p>
If you did not request this action and have received this email in error, you can safely ignore
and discard this email. However, if you continue to receive multiple unsolicited emails of this nature,
we strongly recommend contacting your system administrator to report the issue.
</p>
<p>
<b>Request details:</b>
<br /> Time: <%= @sign_in_token_created_at %>
<br /> IP address: <%= @remote_ip %>
<br /> User Agent: <%= @user_agent %>
<br /> Account ID: <%= @account.id %>
<br /> Account Slug: <%= @account.slug %>
</p>

View File

@@ -0,0 +1,23 @@
Thank you for signing up for Firezone!
Here is the sign-in link to access your account "<%= @account.name %>":
<%= @sign_in_url %>
Please copy this link and open it in your browser. It is valid for 15 minutes.
In future you can always access the sign in form at the following URL:
<%= @sign_in_form_url %>
If you did not request this action and have received this email in error, you can safely ignore
and discard this email. However, if you continue to receive multiple unsolicited emails of this nature,
we strongly recommend contacting your system administrator to report the issue.
Request details:
Time: <%= @sign_in_token_created_at %>
IP address: <%= @remote_ip %>
User Agent: <%= @user_agent %>
Account ID: <%= @account.id %>
Account Slug: <%= @account.slug %>

View File

@@ -1,7 +1,7 @@
defmodule Web.Live.SignUpTest do
use Web.ConnCase, async: true
test "renders signup form", %{conn: conn} do
test "renders sign up form", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/sign_up")
form = form(lv, "form")
@@ -17,7 +17,7 @@ defmodule Web.Live.SignUpTest do
]
end
test "creates new account", %{conn: conn} do
test "creates new account and sends a welcome email", %{conn: conn} do
Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark)
account_name = "FooBar"
@@ -37,6 +37,33 @@ defmodule Web.Live.SignUpTest do
assert html =~ "Your account has been created!"
assert html =~ account_name
account = Repo.one(Domain.Accounts.Account)
assert account.name == account_name
provider = Repo.one(Domain.Auth.Provider)
assert provider.account_id == account.id
actor = Repo.one(Domain.Actors.Actor)
assert actor.account_id == account.id
assert actor.name == "John Doe"
identity = Repo.one(Domain.Auth.Identity)
assert identity.account_id == account.id
assert identity.provider_identifier == "jdoe@test.local"
assert_email_sent(fn email ->
assert email.subject == "Welcome to Firezone"
verify_sign_in_token_path =
~p"/#{account.id}/sign_in/providers/#{provider.id}/verify_sign_in_token"
assert email.text_body =~ "#{verify_sign_in_token_path}"
assert email.text_body =~ "identity_id=#{identity.id}"
assert email.text_body =~ "secret="
assert email.text_body =~ url(~p"/#{account.id}")
end)
end
test "renders changeset errors on input change", %{conn: conn} do
@@ -79,7 +106,7 @@ defmodule Web.Live.SignUpTest do
}
end
test "renders signup disabled message", %{conn: conn} do
test "renders sign up disabled message", %{conn: conn} do
Domain.Config.feature_flag_override(:signups, false)
{:ok, _lv, html} = live(conn, ~p"/sign_up")