From 4eb2c916337cc2c79192f8fd69d635e29b37bfe1 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Thu, 9 Nov 2023 11:41:58 -0600 Subject: [PATCH] Send welcome emails (#2618) And block colliding account slugs from being created. Closes #2599 --- .../lib/domain/accounts/account/changeset.ex | 10 +++++ elixir/apps/web/lib/web/auth.ex | 2 +- elixir/apps/web/lib/web/live/sign_up.ex | 21 ++++++++-- elixir/apps/web/lib/web/mailer/auth_email.ex | 41 ++++++++++++++++++- .../mailer/auth_email/sign_in_link.html.heex | 4 +- .../mailer/auth_email/sign_in_link.text.heex | 2 +- .../mailer/auth_email/sign_up_link.html.heex | 36 ++++++++++++++++ .../mailer/auth_email/sign_up_link.text.heex | 23 +++++++++++ .../test/web/live/sign_up/sign_up_test.exs | 33 +++++++++++++-- 9 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.html.heex create mode 100644 elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.text.heex diff --git a/elixir/apps/domain/lib/domain/accounts/account/changeset.ex b/elixir/apps/domain/lib/domain/accounts/account/changeset.ex index f851948b6..3a98cee29 100644 --- a/elixir/apps/domain/lib/domain/accounts/account/changeset.ex +++ b/elixir/apps/domain/lib/domain/accounts/account/changeset.ex @@ -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"}] diff --git a/elixir/apps/web/lib/web/auth.ex b/elixir/apps/web/lib/web/auth.ex index 716db2895..95addbbc3 100644 --- a/elixir/apps/web/lib/web/auth.ex +++ b/elixir/apps/web/lib/web/auth.ex @@ -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) diff --git a/elixir/apps/web/lib/web/live/sign_up.ex b/elixir/apps/web/lib/web/live/sign_up.ex index 5390ebae4..e7be36216 100644 --- a/elixir/apps/web/lib/web/live/sign_up.ex +++ b/elixir/apps/web/lib/web/live/sign_up.ex @@ -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"""
- Your account has been created! + Your account has been created! Please check your email for a sign in link.
@@ -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} -> diff --git a/elixir/apps/web/lib/web/mailer/auth_email.ex b/elixir/apps/web/lib/web/mailer/auth_email.ex index 9c1fb06c3..118622d54 100644 --- a/elixir/apps/web/lib/web/mailer/auth_email.ex +++ b/elixir/apps/web/lib/web/mailer/auth_email.ex @@ -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)}" ) diff --git a/elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.html.heex b/elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.html.heex index ab50409a3..2afcf0e84 100644 --- a/elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.html.heex +++ b/elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.html.heex @@ -6,13 +6,13 @@

- Here is the magic sign-in link + Here is the magic sign-in link you requested to sign in to "<%= @account.name %>". It is valid for 15 minutes.

- 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 %>
diff --git a/elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.text.heex b/elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.text.heex index 455a367ac..49768ff0c 100644 --- a/elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.text.heex +++ b/elixir/apps/web/lib/web/mailer/auth_email/sign_in_link.text.heex @@ -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 %> diff --git a/elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.html.heex b/elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.html.heex new file mode 100644 index 000000000..e6d58923f --- /dev/null +++ b/elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.html.heex @@ -0,0 +1,36 @@ +

Thank you for signing up for Firezone!

+ +
+

+ Here is the sign-in link + to access your account "<%= @account.name %>". + It is valid for 15 minutes. +

+ + + If the link didn't work, please copy this link and open it in your browser. <%= @sign_in_url %> + +
+ +
+

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 %> +

diff --git a/elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.text.heex b/elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.text.heex new file mode 100644 index 000000000..77f73cee0 --- /dev/null +++ b/elixir/apps/web/lib/web/mailer/auth_email/sign_up_link.text.heex @@ -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 %> diff --git a/elixir/apps/web/test/web/live/sign_up/sign_up_test.exs b/elixir/apps/web/test/web/live/sign_up/sign_up_test.exs index 18dba4897..cf99f52cc 100644 --- a/elixir/apps/web/test/web/live/sign_up/sign_up_test.exs +++ b/elixir/apps/web/test/web/live/sign_up/sign_up_test.exs @@ -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")