Onboarding UX improvements (#441)

* WIP

* Actually activate the user

* Send email verification codes

* Send activation code with email

* Only show onboarding steps during first site creation

* Add worker to config

* Consistent form styles

* Send welcome email when user activates account

* Add changelog entry

* Use https in new site form

* Correct spelling in email
This commit is contained in:
Uku Taht 2020-12-15 11:30:45 +02:00 committed by GitHub
parent 3142e8e190
commit aa7ae87811
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 620 additions and 218 deletions

View File

@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.
- Validate domain format on site creation plausible/analytics#427
- Improve settings UX and design plausible/analytics#412
- Improve site listing UX and design plausible/analytics#438
- Improve onboarding UX and design plausible/analytics#441
### Fixed
- Do not error when activating an already activated account plausible/analytics#370

View File

@ -120,7 +120,9 @@ extra_cron = [
# Every 10 minutes
{"*/10 * * * *", Plausible.Workers.ProvisionSslCertificates},
# Every 15 minutes
{"*/15 * * * *", Plausible.Workers.SpikeNotifier}
{"*/15 * * * *", Plausible.Workers.SpikeNotifier},
# Every day at midnight
{"0 0 * * *", Plausible.Workers.CleanEmailVerificationCodes}
]
base_queues = [rotate_salts: 1]
@ -133,7 +135,8 @@ extra_queues = [
trial_notification_emails: 1,
schedule_email_reports: 1,
send_email_reports: 1,
spike_notifications: 1
spike_notifications: 1,
clean_email_verification_codes: 1
]
config :plausible, Oban,

View File

@ -3,6 +3,41 @@ defmodule Plausible.Auth do
alias Plausible.Auth
alias Plausible.Stats.Clickhouse, as: Stats
def issue_email_verification(user) do
Repo.update_all(from(c in "email_verification_codes", where: c.user_id == ^user.id), [set: [user_id: nil]])
code = Repo.one(from(c in "email_verification_codes", where: is_nil(c.user_id), select: c.code, limit: 1))
Repo.update_all(from(c in "email_verification_codes", where: c.code == ^code), [set: [user_id: user.id, issued_at: Timex.now()]])
code
end
defp is_expired?(activation_code_issued) do
Timex.before?(activation_code_issued, Timex.shift(Timex.now(), hours: -4))
end
def verify_email(user, code) do
found_code = Repo.one(
from c in "email_verification_codes",
where: c.user_id == ^user.id,
where: c.code == ^code,
select: %{code: c.code, issued: c.issued_at}
)
cond do
is_nil(found_code) -> {:error, :incorrect}
is_expired?(found_code[:issued]) -> {:error, :expired}
true ->
{:ok, _} = Ecto.Multi.new
|> Ecto.Multi.update(:user, Plausible.Auth.User.changeset(user, %{email_verified: true}))
|> Ecto.Multi.update_all(:codes, from(c in "email_verification_codes", where: c.user_id == ^user.id), [set: [user_id: nil]])
|> Repo.transaction
:ok
end
end
def create_user(name, email) do
%Auth.User{}
|> Auth.User.new(%{name: name, email: email})

View File

@ -11,15 +11,4 @@ defmodule Plausible.Auth.Token do
max_age: @one_hour_in_seconds
)
end
def sign_activation(name, email) do
Phoenix.Token.sign(PlausibleWeb.Endpoint, "activation", %{
name: name,
email: email
})
end
def verify_activation(token) do
Phoenix.Token.verify(PlausibleWeb.Endpoint, "activation", token, max_age: @one_day_in_seconds)
end
end

View File

@ -8,13 +8,16 @@ defmodule Plausible.Auth.User do
use Ecto.Schema
import Ecto.Changeset
@required [:email, :name, :password, :password_confirmation]
schema "users" do
field :email, :string
field :password_hash
field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true
field :name, :string
field :last_seen, :naive_datetime
field :trial_expiry_date, :date
field :email_verified, :boolean
has_many :site_memberships, Plausible.Site.Membership
has_many :sites, through: [:site_memberships, :site]
@ -26,16 +29,19 @@ defmodule Plausible.Auth.User do
def new(user, attrs \\ %{}) do
user
|> cast(attrs, [:email, :name])
|> validate_required([:email, :name])
|> cast(attrs, @required)
|> validate_required(@required)
|> validate_length(:password, min: 6, message: "has to be at least 6 characters")
|> validate_confirmation(:password)
|> hash_password()
|> change(trial_expiry_date: Timex.today() |> Timex.shift(days: 30))
|> unique_constraint(:email)
end
def changeset(user, attrs \\ %{}) do
user
|> cast(attrs, [:email, :name])
|> validate_required([:email, :name])
|> cast(attrs, [:email, :name, :email_verified])
|> validate_required([:email, :name, :email_verified])
|> unique_constraint(:email)
end
@ -48,4 +54,10 @@ defmodule Plausible.Auth.User do
|> validate_length(:password, min: 6, message: "has to be at least 6 characters")
|> cast(%{password_hash: hash}, [:password_hash])
end
def hash_password(%{errors: [], changes: changes} = changeset) do
hash = Plausible.Auth.Password.hash(changes[:password])
change(changeset, password_hash: hash)
end
def hash_password(changeset), do: changeset
end

View File

@ -25,22 +25,20 @@ defmodule PlausibleWeb.AuthController do
end
def register(conn, params) do
user = Plausible.Auth.User.changeset(%Plausible.Auth.User{}, params["user"])
user = Plausible.Auth.User.new(%Plausible.Auth.User{}, params["user"])
if PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do
case Ecto.Changeset.apply_action(user, :insert) do
case Repo.insert(user) do
{:ok, user} ->
token = Auth.Token.sign_activation(user.name, user.email)
url = PlausibleWeb.Endpoint.url() <> "/claim-activation?token=#{token}"
Logger.info(url)
email_template = PlausibleWeb.Email.activation_email(user, url)
code = Auth.issue_email_verification(user)
Logger.info("VERIFICATION CODE: #{code}")
email_template = PlausibleWeb.Email.activation_email(user, code)
Plausible.Mailer.send_email(email_template)
conn
|> render("register_success.html",
email: user.email,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
|> put_session(:current_user_id, user.id)
|> put_resp_cookie("logged_in", "true", http_only: false)
|> redirect(to: "/activate")
{:error, changeset} ->
render(conn, "register_form.html",
@ -57,37 +55,52 @@ defmodule PlausibleWeb.AuthController do
end
end
def claim_activation_link(conn, %{"token" => token}) do
case Auth.Token.verify_activation(token) do
{:ok, %{name: name, email: email}} ->
case Auth.create_user(name, email) do
{:ok, user} ->
PlausibleWeb.Email.welcome_email(user)
|> Plausible.Mailer.send_email()
def activate_form(conn, _params) do
user = conn.assigns[:current_user]
user_activated_account(conn, user)
has_code = Repo.exists?(
from c in "email_verification_codes",
where: c.user_id == ^user.id
)
{:error, %Ecto.Changeset{errors: [email: {"has already been taken", _}]}} ->
user = Auth.find_user_by(email: email)
user_activated_account(conn, user)
render(conn, "activate.html", has_pin: has_code, layout: {PlausibleWeb.LayoutView, "focus.html"})
end
{:error, changeset} ->
send_resp(conn, 400, inspect(changeset.errors))
end
def activate(conn, %{"code" => code}) do
user = conn.assigns[:current_user]
{code, ""} = Integer.parse(code)
case Auth.verify_email(user, code) do
:ok ->
PlausibleWeb.Email.welcome_email(user)
|> Plausible.Mailer.send_email()
redirect(conn, to: "/sites/new")
{:error, :incorrect} ->
render(conn, "activate.html",
error: "Incorrect activation code",
has_pin: true,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
{:error, :expired} ->
render_error(conn, 401, "Your token has expired. Please request another activation link.")
{:error, _} ->
render_error(conn, 400, "Your token is invalid. Please request another activation link.")
render(conn, "activate.html",
error: "Code is expired, please request another one",
has_pin: false,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end
end
defp user_activated_account(conn, user) do
def request_activation_code(conn, _params) do
user = conn.assigns[:current_user]
pin = Auth.issue_email_verification(user)
email_template = PlausibleWeb.Email.activation_email(user, pin)
Plausible.Mailer.send_email(email_template)
conn
|> put_session(:current_user_id, user.id)
|> put_resp_cookie("logged_in", "true", http_only: false)
|> redirect(to: "/password")
|> put_flash(:success, "Activation code was sent to #{user.email}")
|> redirect(to: "/activate")
end
def password_reset_request_form(conn, _) do

View File

@ -19,9 +19,15 @@ defmodule PlausibleWeb.SiteController do
end
def new(conn, _params) do
current_user = conn.assigns[:current_user]
changeset = Plausible.Site.changeset(%Plausible.Site{})
render(conn, "new.html", changeset: changeset, layout: {PlausibleWeb.LayoutView, "focus.html"})
is_first_site = !Repo.exists?(
from sm in Plausible.Site.Membership,
where: sm.user_id == ^current_user.id
)
render(conn, "new.html", changeset: changeset, is_first_site: is_first_site, layout: {PlausibleWeb.LayoutView, "focus.html"})
end
def create_site(conn, %{"site" => site_params}) do
@ -36,21 +42,34 @@ defmodule PlausibleWeb.SiteController do
|> redirect(to: "/#{URI.encode_www_form(site.domain)}/snippet")
{:error, :site, changeset, _} ->
is_first_site = !Repo.exists?(
from sm in Plausible.Site.Membership,
where: sm.user_id == ^user.id
)
render(conn, "new.html",
changeset: changeset,
is_first_site: is_first_site,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end
end
def add_snippet(conn, %{"website" => website}) do
user = conn.assigns[:current_user]
site =
Sites.get_for_user!(conn.assigns[:current_user].id, website)
|> Repo.preload(:custom_domain)
is_first_site = !Repo.exists?(
from sm in Plausible.Site.Membership,
where: sm.user_id == ^user.id
and sm.site_id != ^site.id
)
conn
|> assign(:skip_plausible_tracking, true)
|> render("snippet.html", site: site, layout: {PlausibleWeb.LayoutView, "focus.html"})
|> render("snippet.html", site: site, is_first_site: is_first_site, layout: {PlausibleWeb.LayoutView, "focus.html"})
end
def new_goal(conn, %{"website" => website}) do

View File

@ -6,12 +6,12 @@ defmodule PlausibleWeb.Email do
Application.get_env(:plausible, :mailer_email)
end
def activation_email(user, link) do
def activation_email(user, code) do
base_email()
|> to(user.email)
|> to(user)
|> tag("activation-email")
|> subject("Activate your Plausible free trial")
|> render("activation_email.html", name: user.name, link: link)
|> subject("#{code} is your Plausible email verification code")
|> render("activation_email.html", user: user, code: code)
end
def welcome_email(user) do
base_email()

View File

@ -6,13 +6,20 @@ defmodule PlausibleWeb.RequireAccountPlug do
end
def call(conn, _opts) do
case conn.assigns[:current_user] do
nil ->
user = conn.assigns[:current_user]
cond do
is_nil(user) ->
Plug.Conn.put_session(conn, :login_dest, conn.request_path)
|> Phoenix.Controller.redirect(to: "/login")
|> halt
_email ->
not user.email_verified ->
conn
|> Phoenix.Controller.redirect(to: "/activate")
|> halt
true ->
conn
end
end

View File

@ -77,7 +77,9 @@ defmodule PlausibleWeb.Router do
get "/register", AuthController, :register_form
post "/register", AuthController, :register
get "/claim-activation", AuthController, :claim_activation_link
get "/activate", AuthController, :activate_form
post "/activate/request-code", AuthController, :request_activation_code
post "/activate", AuthController, :activate
get "/login", AuthController, :login_form
post "/login", AuthController, :login
get "/password/request-reset", AuthController, :password_reset_request_form

View File

@ -0,0 +1,39 @@
<div class="pt-6 px-4 sm:px-6 lg:px-8">
<nav class="flex justify-center">
<ol class="space-y-6">
<%= for {step, index} <- Enum.with_index(["Register", "Activate account", "Add site info", "Install snippet"]) do %>
<%= if index < @current_step do %>
<!-- Complete Step -->
<li class="flex items-start">
<span class="flex-shrink-0 relative h-5 w-5 flex items-center justify-center">
<svg class="h-full w-full text-indigo-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</span>
<span class="ml-3 text-sm font-medium text-gray-500"><%= step %></span>
</li>
<% end %>
<%= if index == @current_step do %>
<!-- Current Step -->
<li class="flex items-start">
<span class="flex-shrink-0 h-5 w-5 relative flex items-center justify-center">
<span class="absolute h-4 w-4 rounded-full bg-indigo-200"></span>
<span class="relative block w-2 h-2 bg-indigo-600 rounded-full"></span>
</span>
<span class="ml-3 text-sm font-medium text-indigo-600"><%= step %></span>
</li>
<% end %>
<%= if index > @current_step do %>
<!-- Upcoming Step -->
<li class="flex items-start">
<div class="flex-shrink-0 h-5 w-5 relative flex items-center justify-center">
<div class="h-2 w-2 bg-gray-300 rounded-full"></div>
</div>
<p class="ml-3 text-sm font-medium text-gray-500"><%= step %></p>
</li>
<% end %>
<% end %>
</ol>
</nav>
</div>

View File

@ -0,0 +1,43 @@
<div class="w-full max-w-3xl mt-4 mx-auto flex">
<%= if @has_pin do %>
<%= form_for @conn, "/activate", [class: "w-full max-w-lg mx-auto bg-white shadow-md rounded px-8 py-6 mb-4 mt-8"], fn f -> %>
<h2 class="text-xl font-black">Activate your account</h2>
<div class="mt-2 text-sm text-gray-500 leading-tight">
Please enter the 4-digit code we sent to <b><%= @conn.assigns[:current_user].email %></b>
</div>
<div class="mt-12 flex items-stretch flex-grow">
<div>
<%= text_input f, :code, class: "tracking-widest font-medium shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-36 px-8 border-gray-300 rounded-l-md", oninput: "this.value=this.value.replace(/[^0-9]/g, ''); if (this.value.length >= 4) document.getElementById('submit').focus()", onclick: "this.select();", maxlength: "4", placeholder: "••••", style: "letter-spacing: 10px;" %>
</div>
<button id="submit" class="button rounded-l-none">Activate &rarr;</button>
</div>
<%= error_tag(assigns, :error) %>
<div class="mt-16 text-sm">
Didn't receive an email?
</div>
<div class="mt-2 text-sm text-gray-500 leading-tight">
Please check your spam folder and contact <a class="underline text-gray-800" href="mailto:support@plausible.io">support@plausible.io</a> if the problem persists
</div>
<% end %>
<% else %>
<div class="w-full max-w-lg mx-auto bg-white shadow-md rounded px-8 py-6 mb-4 mt-8">
<h2 class="text-xl font-black">Activate your account</h2>
<div class="mt-2 text-sm text-gray-500 leading-tight">
A 4-digit activation code will be sent to <b><%= @conn.assigns[:current_user].email %></b>
</div>
<%= error_tag(assigns, :error) %>
<%= button("Request activation code", to: "/activate/request-code", method: :post, class: "button mt-12") %>
</div>
<% end %>
<div class="pt-12 pl-8 hidden md:block">
<%= render(PlausibleWeb.AuthView, "_onboarding_steps.html", current_step: 1) %>
</div>
</div>

View File

@ -18,7 +18,7 @@
<%= submit "Login →", class: "button mt-4 w-full" %>
<%= if !Keyword.fetch!(Application.get_env(:plausible, :selfhost),:disable_registration) do %>
<p class="text-center text-gray-500 text-xs mt-4">
Don't have an account? <%= link("Register", to: "/register") %> instead.
Don't have an account? <%= link("Register", to: "/register", class: "underline text-gray-800") %> instead.
</p>
<% end %>
<% end %>

View File

@ -3,69 +3,63 @@
<div class="text-xl font-medium">Set up privacy-friendly analytics with just a few clicks</div>
</div>
<div>
<div class="w-full max-w-3xl mt-4 mx-auto flex">
<%= form_for @changeset, "/register", [class: "w-full max-w-md mx-auto bg-white shadow-md rounded px-8 py-6 mb-4 mt-8", id: "register-form"], fn f -> %>
<h2 class="text-xl font-black">Enter your details</h2>
<div class="my-4">
<%= label f, :name, "Full name", class: "block text-gray-700 text-sm font-bold mb-2" %>
<%= text_input f, :name, class: "transition bg-gray-100 appearance-none border border-transparent rounded w-full p-2 text-gray-700 leading-normal appearance-none focus:outline-none focus:bg-white focus:border-gray-300", placeholder: "Jane Doe" %>
<%= error_tag f, :name %>
<div class="w-full max-w-3xl mt-4 mx-auto flex">
<%= form_for @changeset, "/register", [class: "w-full max-w-md mx-auto bg-white shadow-md rounded px-8 py-6 mb-4 mt-8", id: "register-form"], fn f -> %>
<h2 class="text-xl font-black">Enter your details</h2>
<div class="my-4">
<%= label f, :name, "Full name", class: "block text-sm font-medium text-gray-700" %>
<div class="mt-1">
<%= text_input f, :name, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md", placeholder: "Jane Doe" %>
</div>
<div class="my-4">
<%= label f, :email, class: "block text-gray-700 text-sm font-bold mb-2" %>
<p class="text-gray-600 text-xs mt-1 mb-2">No spam, guaranteed.</p>
<%= email_input f, :email, class: "transition bg-gray-100 appearance-none border border-transparent rounded w-full p-2 text-gray-700 leading-normal appearance-none focus:outline-none focus:bg-white focus:border-gray-300", placeholder: "example@email.com" %>
<%= error_tag f, :email %>
</div>
<%= if PlausibleWeb.Captcha.enabled?() do %>
<div class="mt-4">
<div class="h-captcha" data-sitekey="<%= PlausibleWeb.Captcha.sitekey() %>"></div>
<%= if assigns[:captcha_error] do %>
<div class="text-red-500 text-xs italic mt-3"><%= @captcha_error %></div>
<% end %>
<script src="https://hcaptcha.com/1/api.js" async defer></script>
</div>
<% end %>
<%= submit "Start my free trial →", class: "button mt-4 w-full" %>
<p class="text-center text-gray-600 text-xs mt-4">
Already have an account? <%= link("Log in", to: "/login", class: "underline text-gray-800") %> instead.
</p>
<% end %>
<div class="pt-12 pl-8 hidden md:block">
<ul class="mt-2 leading-loose">
<li>
<svg class="feather text-indigo-600 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
Quick and easy to set up
</li>
<li>
<svg class="feather text-indigo-600 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
No credit card required
</li>
<li>
<svg class="feather text-indigo-600 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
Unlimited use during the trial
</li>
<li>
<svg class="feather text-indigo-600 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
From just $6/mo after the trial
</li>
</ul>
<%= error_tag f, :name %>
</div>
<div class="my-4">
<div class="flex justify-between">
<%= label f, :email, class: "block text-sm font-medium text-gray-700" %>
<p class="text-xs text-gray-500 mt-1">No spam, guaranteed.</p>
</div>
<div class="mt-1">
<%= email_input f, :email, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md", placeholder: "example@email.com" %>
</div>
<%= error_tag f, :email %>
</div>
<div class="my-4">
<div class="flex justify-between">
<%= label f, :password, class: "block text-sm font-medium text-gray-700" %>
<p class="text-xs text-gray-500 mt-1">Min 6 characters</p>
</div>
<div class="mt-1">
<%= password_input f, :password, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
</div>
<%= error_tag f, :password %>
</div>
<div class="my-4">
<%= label f, :password_confirmation, class: "block text-sm font-medium text-gray-700" %>
<div class="mt-1">
<%= password_input f, :password_confirmation, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
</div>
<%= error_tag f, :password_confirmation %>
</div>
<%= if PlausibleWeb.Captcha.enabled?() do %>
<div class="mt-4">
<div class="h-captcha" data-sitekey="<%= PlausibleWeb.Captcha.sitekey() %>"></div>
<%= if assigns[:captcha_error] do %>
<div class="text-red-500 text-xs italic mt-3"><%= @captcha_error %></div>
<% end %>
<script src="https://hcaptcha.com/1/api.js" async defer></script>
</div>
<% end %>
<%= submit "Start my free trial →", class: "button mt-4 w-full" %>
<p class="text-center text-gray-600 text-xs mt-4">
Already have an account? <%= link("Log in", to: "/login", class: "underline text-gray-800") %> instead.
</p>
<% end %>
<div class="pt-12 pl-8 hidden md:block">
<%= render(PlausibleWeb.AuthView, "_onboarding_steps.html", current_step: 0) %>
</div>
</div>

View File

@ -1,12 +1,34 @@
<div class="bg-white max-w-md w-full mx-auto shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8">
<h2 class="text-xl font-black">Success!</h2>
<div class="my-4 leading-tight">
We've sent an activation link to <b><%= @email %></b>. Please click on the link to activate your account.
</div>
<div class="mt-8 text-sm">
Didn't receive an email?
</div>
<div class="mt-2 text-sm text-gray-600 leading-tight">
Please check your spam folder and contact <a href="mailto:support@plausible.io">support@plausible.io</a> if the problem persists
<div class="mx-auto mt-6 text-center">
<h1 class="text-3xl font-black">Register your 30-day unlimited-use free trial</h1>
<div class="text-xl font-medium">Set up privacy-friendly analytics with just a few clicks</div>
</div>
<div class="w-full max-w-3xl mt-4 mx-auto flex">
<%= form_for @conn, "/claim-activation", [class: "w-full max-w-lg mx-auto bg-white shadow-md rounded px-8 py-6 mb-4 mt-8", id: "register-form"], fn f -> %>
<h2 class="text-xl font-black">Activate your account</h2>
<div class="mt-2 text-sm text-gray-500 leading-tight">
Please enter the 4-digit code we sent to <b><%= @email %></b>
</div>
<div class="mt-12 flex items-stretch flex-grow">
<div>
<%= text_input f, :code, class: "tracking-widest font-medium shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-36 px-8 border-gray-300 rounded-l-md", oninput: "this.value=this.value.replace(/[^0-9]/g, ''); if (this.value.length >= 4) document.getElementById('submit').focus()", onclick: "this.select();", maxlength: "4", placeholder: "••••", style: "letter-spacing: 10px;" %>
<%= error_tag f, :name %>
</div>
<button id="submit" class="button rounded-l-none">Activate &rarr;</button>
</div>
<div class="mt-16 text-sm">
Didn't receive an email?
</div>
<div class="mt-2 text-sm text-gray-500 leading-tight">
Please check your spam folder and contact <a class="underline text-gray-800" href="mailto:support@plausible.io">support@plausible.io</a> if the problem persists
</div>
<% end %>
<div class="pt-12 pl-8 hidden md:block">
<%= render(PlausibleWeb.AuthView, "_onboarding_steps.html", current_step: 1) %>
</div>
</div>

View File

@ -97,14 +97,18 @@
<%= form_for @changeset, "/settings", [class: "max-w-sm"], fn f -> %>
<div class="my-4">
<%= label f, :name, class: "block text-gray-700 text-sm font-bold mb-2" %>
<%= text_input f, :name, class: "transition bg-gray-100 appearance-none border border-transparent rounded w-full p-2 text-gray-700 leading-normal appearance-none focus:outline-none focus:border-gray-300" %>
<%= error_tag f, :name %>
<%= label f, :name, class: "block text-sm font-medium text-gray-700" %>
<div class="mt-1">
<%= text_input f, :name, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
<%= error_tag f, :name %>
</div>
</div>
<div class="my-4">
<%= label f, :email, class: "block text-gray-700 text-sm font-bold mb-2" %>
<%= email_input f, :email, class: "transition bg-gray-100 appearance-none border border-transparent rounded w-full p-2 text-gray-700 leading-normal appearance-none focus:outline-none focus:border-gray-300" %>
<%= error_tag f, :email %>
<%= label f, :email, class: "block text-sm font-medium text-gray-700" %>
<div class="mt-1">
<%= email_input f, :email, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
<%= error_tag f, :email %>
</div>
</div>
<%= submit "Save changes", class: "button mt-4" %>
<% end %>

View File

@ -1,5 +1,3 @@
Hi <%= @name %>,
Hey <%= user_salutation(@user) %>,
<br /><br />
Thank you for signing up for the free trial of Plausible, a simple and privacy-friendly alternative to Google Analytics.
<br /><br />
<a href="<%= @link %>">Click here</a> to activate your trial account. This link will expire in 24 hours.
Enter <%= @code %> to verify your email address. This code will expire in 4 hours.

View File

@ -5,7 +5,6 @@
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<!-- Heroicon name: check-circle -->
<svg class="h-6 w-6 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@ -32,3 +31,35 @@
</div>
</div>
<% end %>
<%= if get_flash(@conn, :error) do %>
<div class="z-50 fixed inset-0 flex items-end justify-center px-4 py-6 pointer-events-none sm:p-6 sm:items-start sm:justify-end">
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 4000)" x-transition:enter="transform ease-out duration-300 transition" x-transition:enter-start="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2" x-transition:enter-end="translate-y-0 opacity-100 sm:translate-x-0" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto">
<div class="rounded-lg ring-1 ring-black ring-opacity-5 overflow-hidden">
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="w-6 h-6 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm leading-5 font-medium text-gray-900">
<%= get_flash(@conn, :error_title) || "Error" %>
</p>
<p class="mt-1 text-sm leading-5 text-gray-500">
<%= get_flash(@conn, :error) %>
</p>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button class="inline-flex text-gray-400 focus:outline-none focus:text-gray-500 transition ease-in-out duration-150" @click="show = false">
<!-- Heroicon name: x -->
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<% end %>

View File

@ -17,6 +17,7 @@
</a>
</div>
<%= render("_flash.html", assigns) %>
<%= render @view_module, @view_template, assigns %>
<p class="text-center text-gray-500 text-xs py-8">

View File

@ -1,27 +1,36 @@
<%= form_for @changeset, "/sites", [class: "max-w-md w-full mx-auto bg-white shadow-lg rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
<h2 class="text-xl font-bold">Your website details</h2>
<div class="my-6">
<%= label f, :domain, class: "block text-gray-700 text-sm font-bold" %>
<p class="text-gray-600 text-xs mt-1 mb-2">Just the naked domain without <code>www</code> or <code>https://</code></p>
<%= text_input f, :domain, class: "transition bg-gray-100 appearance-none border border-transparent rounded w-full p-2 text-gray-700 leading-normal appearance-none focus:outline-none focus:bg-white focus:border-gray-400", placeholder: "example.com" %>
<%= error_tag f, :domain %>
</div>
<div class="my-6">
<%= label f, :timezone, "Reporting Timezone", class: "block text-gray-700 text-sm font-bold" %>
<p class="text-gray-600 text-xs mt-1 mb-2">To make sure we agree on what 'today' means</p>
<div class="w-full max-w-3xl mt-4 mx-auto flex">
<%= form_for @changeset, "/sites", [class: "max-w-lg w-full mx-auto bg-white shadow-lg rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
<h2 class="text-xl font-black">Your website details</h2>
<div class="my-6">
<%= label f, :domain, class: "block text-sm font-medium text-gray-700" %>
<div class="mt-2 flex rounded-md shadow-sm">
<span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
https://
</span>
<%= text_input f, :domain, class: "focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full px-3 py-2 rounded-none rounded-r-md sm:text-sm border-gray-300", placeholder: "example.com" %>
</div>
<%= error_tag f, :domain %>
</div>
<div class="my-6">
<%= label f, :timezone, "Reporting Timezone", class: "block text-sm font-medium text-gray-700" %>
<p class="text-gray-500 text-xs mt-1">To make sure we agree on what 'today' means</p>
<div class="inline-block relative w-full">
<%= select f, :timezone, Plausible.Timezones.options(), id: "tz-select", selected: "Etc/Greenwich", class: "block appearance-none w-full bg-gray-100 text-gray-700 cursor-pointer hover:border-gray-500 p-2 pr-8 rounded leading-normal focus:outline-none" %>
<div class="pointer-events-none absolute top-0 right-0 flex items-center px-2 text-red">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg>
<div class="inline-block relative w-full">
<%= select f, :timezone, Plausible.Timezones.options(), id: "tz-select", selected: "Etc/Greenwich", class: "mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md" %>
</div>
</div>
</div>
<script>
var offset = (new Date()).getTimezoneOffset()
var option = document.querySelector('#tz-select option[offset="' + offset + '"]')
if (option) { option.selected = "selected"}
</script>
<script>
var offset = (new Date()).getTimezoneOffset()
var option = document.querySelector('#tz-select option[offset="' + offset + '"]')
if (option) { option.selected = "selected"}
</script>
<%= submit "Add snippet →", class: "button mt-4 w-full" %>
<% end %>
<%= submit "Add snippet →", class: "button mt-4 w-full" %>
<% end %>
<%= if @is_first_site do %>
<div class="pt-12 pl-8 hidden md:block">
<%= render(PlausibleWeb.AuthView, "_onboarding_steps.html", current_step: 2) %>
</div>
<% end %>
</div>

View File

@ -1,14 +1,22 @@
<%= form_for @conn, "/", [class: "max-w-md w-full mx-auto bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
<h2 class="text-xl font-bold">Add javascript snippet</h2>
<div class="my-4">
<p>Paste this snippet in the <code>&lt;head&gt;</code> of your website.</p>
<div class="w-full max-w-3xl mt-4 mx-auto flex">
<%= form_for @conn, "/", [class: "max-w-lg w-full mx-auto bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
<h2 class="text-xl font-bold">Add javascript snippet</h2>
<div class="my-4">
<p>Paste this snippet in the <code>&lt;head&gt;</code> of your website.</p>
<div class="relative">
<%= textarea f, :domain, id: "snippet_code", class: "transition overflow-hidden bg-gray-100 appearance-none border border-transparent rounded w-full p-2 pr-6 text-gray-700 leading-normal appearance-none focus:outline-none focus:bg-white focus:border-gray-400 text-xs mt-4 resize-none", value: snippet(@site), rows: 3, readonly: "readonly" %>
<a onclick="var textarea = document.getElementById('snippet_code'); textarea.focus(); textarea.select(); document.execCommand('copy');" href="javascript:void(0)" class="no-underline text-indigo-500 text-sm hover:underline">
<svg class="absolute text-indigo-500" style="top: 24px; right: 12px;" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</a>
<div class="relative">
<%= textarea f, :domain, id: "snippet_code", class: "transition overflow-hidden bg-gray-100 appearance-none border border-transparent rounded w-full p-2 pr-6 text-gray-700 leading-normal appearance-none focus:outline-none focus:bg-white focus:border-gray-400 text-xs mt-4 resize-none", value: snippet(@site), rows: 3, readonly: "readonly" %>
<a onclick="var textarea = document.getElementById('snippet_code'); textarea.focus(); textarea.select(); document.execCommand('copy');" href="javascript:void(0)" class="no-underline text-indigo-500 text-sm hover:underline">
<svg class="absolute text-indigo-500" style="top: 24px; right: 12px;" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</a>
</div>
</div>
</div>
<%= link("Start collecting data →", class: "button mt-4 w-full", to: "/#{URI.encode_www_form(@site.domain)}") %>
<% end %>
<%= link("Start collecting data →", class: "button mt-4 w-full", to: "/#{URI.encode_www_form(@site.domain)}") %>
<% end %>
<%= if @is_first_site do %>
<div class="pt-12 pl-8 hidden md:block">
<%= render(PlausibleWeb.AuthView, "_onboarding_steps.html", current_step: 3) %>
</div>
<% end %>
</div>

View File

@ -13,15 +13,15 @@
setInterval(updateStatus, 1500)
</script>
<div class="w-full max-w-sm mx-auto mt-8">
<div class="w-full max-w-md mx-auto mt-8">
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-16 relative text-center">
<h2 class="text-xl font-bold">Waiting for first pageview</h2>
<h2 class="text-xl font-bold">on <%= @site.domain %></h2>
<div class="my-44">
<div class="block pulsating-circle"></div>
<p class="text-gray-600 text-sm absolute left-0 bottom-0 mb-6 w-full text-center leading-normal">
<p class="text-gray-500 text-xs absolute left-0 bottom-0 mb-6 w-full text-center leading-normal">
Need to see the snippet again? <%= link("Click here", to: "/#{URI.encode_www_form(@site.domain)}/snippet")%><br />
Not working? Contact <a href="mailto:support@plausible.io">support@plausible.io</a> to get set up
Not working? Contact <a href="mailto:support@plausible.io" class="text-gray-700 underline">support@plausible.io</a> to get set up
</p>
</div>
</div>

View File

@ -1,9 +1,17 @@
defmodule PlausibleWeb.ErrorHelpers do
use Phoenix.HTML
def error_tag(form, field) do
def error_tag(%Phoenix.HTML.Form{} = form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:div, elem(error, 0), class: "text-red-500 text-xs italic mt-3")
content_tag(:div, elem(error, 0), class: "mt-2 text-sm text-red-600")
end)
end
def error_tag(assigns, field) when is_map(assigns) do
error = assigns[field]
if error do
content_tag(:div, error, class: "mt-2 text-sm text-red-600")
end
end
end

View File

@ -0,0 +1,15 @@
defmodule Plausible.Workers.CleanEmailVerificationCodes do
use Plausible.Repo
use Oban.Worker, queue: :clean_email_verification_codes
@impl Oban.Worker
def perform(_args, _job) do
Repo.update_all(
from(c in "email_verification_codes",
where: not is_nil(c.user_id),
where: c.issued_at < fragment("now() - INTERVAL '4 hours'")
),
[set: [user_id: nil]]
)
end
end

View File

@ -73,7 +73,7 @@ defmodule Plausible.MixProject do
{:ref_inspector, "~> 1.3"},
{:timex, "~> 3.6"},
{:ua_inspector, "~> 0.18"},
{:bamboo, "~> 1.3"},
{:bamboo, "~> 1.6"},
{:bamboo_postmark, "~> 0.5"},
{:bamboo_smtp, "~> 2.1.0"},
{:sentry, "~> 7.0"},
@ -92,7 +92,7 @@ defmodule Plausible.MixProject do
{:geolix, "~> 1.0"},
{:clickhouse_ecto, git: "https://github.com/plausible/clickhouse_ecto.git"},
{:geolix_adapter_mmdb2, "~> 0.5.0"},
{:mix_test_watch, "~> 1.0", only: :dev}
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false}
]
end

View File

@ -2,13 +2,13 @@
"appsignal": {:hex, :appsignal, "2.0.1", "1e68f35fb1771684f0033205c912d70bcd16b1c474c32bfd7439b14a43ff1424", [:make, :mix], [{:decorator, "~> 1.2.3 or ~> 1.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, ">= 1.3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "930c3373b77d4cd94a6a6ea912f7b7c0c4a46ea5302001b9a86ebbb484dd88e5"},
"appsignal_phoenix": {:hex, :appsignal_phoenix, "2.0.2", "17d58f3ddfbc7dd78177f83cf6c5e7531c78742d9d46ff6db33a712ccd2ec098", [:mix], [{:appsignal_plug, "~> 2.0.0", [hex: :appsignal_plug, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.11", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.9", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d41e3892f1cbbdc1762722ea36ff224b087a5fc564fa312ef4c8327391326b96"},
"appsignal_plug": {:hex, :appsignal_plug, "2.0.2", "18363dfa34c5e6aef7c0c71ea38a6e0dcf2d16e06c47bb53a125fc8d9de09a32", [:mix], [{:appsignal, "~> 2.0.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:plug, ">= 1.1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd36e4232973de1e3b537cd8c96e1514ee43a5f58a02702b6d03f7d70d0ce17d"},
"bamboo": {:hex, :bamboo, "1.5.0", "1926107d58adba6620450f254dfe8a3686637a291851fba125686fa8574842af", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d5f3d04d154e80176fd685e2531e73870d8700679f14d25a567e448abce6298d"},
"bamboo": {:hex, :bamboo, "1.6.0", "adfb583bef028923aae6f22deaea6667290561da1246058556ecaeb0fec5a175", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "454e67feacbc9b6e00553ce1d2fba003c861e0035600d59b09d6159985b17f9b"},
"bamboo_postmark": {:hex, :bamboo_postmark, "0.6.0", "429ee3153497e2f1081f8741242450be13cdca52e2c56166e8eda5ebfcb23c0a", [:mix], [{:bamboo, ">= 1.2.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:hackney, ">= 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "badb3c7677440f641d920e0900ff0cd13c01d517e76562aa5c00e560f46f36ba"},
"bamboo_smtp": {:hex, :bamboo_smtp, "2.1.0", "4be58f3c51d9f7875dc169ae58a1d2f08e5b718bf3895f70d130548c0598f422", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.15.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "0aad00ef93d0e0c83a0e1ca6998fea070c8a720a990fbda13ce834136215ee49"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.2.0", "3df902b81ce7fa8867a2ae30d20a1da6877a2c056bfb116fd0bc8a5f0190cea4", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "762be3fcb779f08207531bc6612cca480a338e4b4357abb49f5ce00240a77d1e"},
"bertex": {:hex, :bertex, "1.3.0", "0ad0df9159b5110d9d2b6654f72fbf42a54884ef43b6b651e6224c0af30ba3cb", [:mix], [], "hexpm", "0a5d5e478bb5764b7b7bae37cae1ca491200e58b089df121a2fe1c223d8ee57a"},
"browser": {:hex, :browser, "0.4.4", "bd6436961a6b2299c6cb38d0e49761c1161d869cd0db46369cef2bf6b77c3665", [:mix], [{:plug, "~> 1.2", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d476ca309d4a4b19742b870380390aabbcb323c1f6f8745e2da2dfd079b4f8d7"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"clickhouse_ecto": {:git, "https://github.com/plausible/clickhouse_ecto.git", "ac7514c9155378bde3be34c2a4598fb227367b15", []},
"clickhousex": {:git, "https://github.com/plausible/clickhousex", "89d58d4cb0cad2558e874f30e81a5c2c84ada95e", []},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
@ -33,16 +33,16 @@
"geolix": {:hex, :geolix, "1.1.0", "8b0fe847fef486d9e8b7c21eae6cbc2d998fb249e61d3f4f136f8016b9c1c833", [:mix], [{:poolboy, "~> 1.0", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "980854f2aef30c288dc79e86c5267806d704c4525fde1b75de9a92f67fb16300"},
"geolix_adapter_mmdb2": {:hex, :geolix_adapter_mmdb2, "0.5.0", "5912723d9538ecddc6b29b1d8041b917b735a78fd3c122bfea8c44aa782e3369", [:mix], [{:geolix, "~> 1.1", [hex: :geolix, repo: "hexpm", optional: false]}, {:mmdb2_decoder, "~> 3.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}], "hexpm", "cb1485b6a0a2d3e541949207428a245718dbf1258453a0df0e5fdd925bcecd3e"},
"gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
"httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"},
"joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
"junit_formatter": {:hex, :junit_formatter, "3.1.0", "3f69c61c5413750f9c45e367d77aabbeac9b395acf478d8e70b4ee9d1989c709", [:mix], [], "hexpm", "da52401a93f711fc4f77ffabdda68f9a16fcad5d96f5fce4ae606ab1d73b72f4"},
"logflare_logger_backend": {:hex, :logflare_logger_backend, "0.7.6", "5e61ffbc1716a934d0689a8575b8dc5c5150923cea64fbca684723bf82e335dd", [:mix], [{:bertex, "~> 1.3", [hex: :bertex, repo: "hexpm", optional: false]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}, {:typed_struct, ">= 0.0.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "318363b27150f3f7973352b8b9c985fc303e97e54c04f146ccba706931d31f83"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"},
"mmdb2_decoder": {:hex, :mmdb2_decoder, "3.0.0", "54828676a36e75e9a25bc9a0bb0598d4c7fcc767bf0b40674850b22e05b7b6cc", [:mix], [], "hexpm", "359dc9242915538d1dceb9f6d96c72201dca76ce62e49d22e2ed1e86f20bea8e"},
@ -57,9 +57,9 @@
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.2", "38d94c30df5e2ef11000697a4fbe2b38d0fbf79239d492ff1be87bbc33bc3a84", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "a3dec3d28ddb5476c96a7c8a38ea8437923408bc88da43e5c45d97037b396280"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"},
"php_serializer": {:hex, :php_serializer, "0.9.2", "59c5fd6bd3096671fd89358fb8229341ac7423b50ad8d45a15213b02ea2edab2", [:mix], [], "hexpm", "34eb835a460944f7fc216773b363c02e7dcf8ac0390c9e9ccdbd92b31a7ca59a"},
"plug": {:hex, :plug, "1.10.1", "c56a6d9da7042d581159bcbaef873ba9d87f15dce85420b0d287bca19f40f9bd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b5cd52259817eb8a31f2454912ba1cff4990bca7811918878091cb2ab9e52cb8"},
"plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"},
"plug_cowboy": {:hex, :plug_cowboy, "2.2.1", "fcf58aa33227a4322a050e4783ee99c63c031a2e7f9a2eb7340d55505e17f30f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b43de24460d87c0971887286e7a20d40462e48eb7235954681a20cee25ddeb6"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"},
@ -68,13 +68,13 @@
"sentry": {:hex, :sentry, "7.2.4", "b5bc90b594d40c2e653581e797a5fd2fdf994f2568f6bd66b7fa4971598be8d5", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "4ee4d368b5013076afcc8b73ed028bdc8ee9db84ea987e3591101e194c1fc24b"},
"siphash": {:hex, :siphash, "3.2.0", "ec03fd4066259218c85e2a4b8eec4bb9663bc02b127ea8a0836db376ba73f2ed", [:make, :mix], [], "hexpm", "ba3810701c6e95637a745e186e8a4899087c3b079ba88fb8f33df054c3b0b7c3"},
"sshex": {:hex, :sshex, "2.2.1", "e1270b8345ea2a66a11c2bb7aed22c93e3bc7bc813486f4ffd0a980e4a898160", [:mix], [], "hexpm", "45b2caa5011dc850e70a2d77e3b62678a3e8bcb903eab6f3e7afb2ea897b13db"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, 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", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"},
"timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"},
"typed_struct": {:hex, :typed_struct, "0.2.1", "e1993414c371f09ff25231393b6430bd89d780e2a499ae3b2d2b00852f593d97", [:mix], [], "hexpm", "8f5218c35ec38262f627b2c522542f1eae41f625f92649c0af701a6fab2e11b3"},
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
"ua_inspector": {:hex, :ua_inspector, "0.20.0", "01939baf5706f7d6c2dc0affbbd7f5e14309ba43ebf8967aa6479ee2204f23bc", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.0", [hex: :poolboy, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "30e8623b9f55e7d58be12fc2afd50be8792ec14192c289701d3cc93ad6027f26"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
"yamerl": {:hex, :yamerl, "0.8.0", "8214cfe16bbabe5d1d6c14a14aea11c784b9a21903dd6a7c74f8ce180adae5c7", [:rebar3], [], "hexpm", "010634477bf9c208a0767dcca89116c2442cf0b5e87f9c870f85cd1c3e0c2aab"},
}

View File

@ -0,0 +1,17 @@
defmodule Plausible.Repo.Migrations.AddEmailVerificationCodes do
use Ecto.Migration
def up do
create table(:email_verification_codes, primary_key: false) do
add :code, :integer, null: false
add :user_id, references(:users, on_delete: :delete_all)
add :issued_at, :naive_datetime
end
execute "INSERT INTO email_verification_codes (code) SELECT code FROM GENERATE_SERIES (1000, 9999) AS s(code) order by random();"
end
def down do
drop table(:email_verification_codes)
end
end

View File

@ -0,0 +1,14 @@
defmodule Plausible.Repo.Migrations.AddEmailVerifiedToUsers do
use Ecto.Migration
use Plausible.Repo
def change do
alter table(:users) do
add :email_verified, :boolean, null: false, default: false
end
flush()
Repo.update_all("users", [set: [email_verified: true]])
end
end

View File

@ -1,6 +1,7 @@
defmodule PlausibleWeb.AuthControllerTest do
use PlausibleWeb.ConnCase
use Bamboo.Test
use Plausible.Repo
import Plausible.TestUtils
describe "GET /register" do
@ -9,60 +10,148 @@ defmodule PlausibleWeb.AuthControllerTest do
assert html_response(conn, 200) =~ "Enter your details"
end
end
describe "POST /register" do
test "registering sends an activation link", %{conn: conn} do
post(conn, "/register",
user: %{
name: "Jane Doe",
email: "user@example.com"
email: "user@example.com",
password: "very-secret",
password_confirmation: "very-secret"
}
)
assert_email_delivered_with(subject: "Activate your Plausible free trial")
assert_delivered_email_matches(%{to: [{_, user_email}], subject: subject})
assert user_email == "user@example.com"
assert subject =~ "is your Plausible email verification code"
end
test "user sees success page after registering", %{conn: conn} do
test "creates user record", %{conn: conn} do
post(conn, "/register",
user: %{
name: "Jane Doe",
email: "user@example.com",
password: "very-secret",
password_confirmation: "very-secret"
}
)
user = Repo.one(Plausible.Auth.User)
assert user.name == "Jane Doe"
end
test "logs the user in", %{conn: conn} do
conn =
post(conn, "/register",
user: %{
name: "Jane Doe",
email: "user@example.com"
email: "user@example.com",
password: "very-secret",
password_confirmation: "very-secret",
}
)
assert html_response(conn, 200) =~ "Success!"
assert get_session(conn, :current_user_id)
end
test "user is redirected to activation after registration", %{conn: conn} do
conn =
post(conn, "/register",
user: %{
name: "Jane Doe",
email: "user@example.com",
password: "very-secret",
password_confirmation: "very-secret",
}
)
assert redirected_to(conn) == "/activate"
end
end
describe "GET /claim-activation" do
test "creates the user", %{conn: conn} do
token = Plausible.Auth.Token.sign_activation("Jane Doe", "user@example.com")
get(conn, "/claim-activation?token=#{token}")
describe "GET /activate" do
setup [:create_user, :log_in]
assert Plausible.Auth.find_user_by(email: "user@example.com")
test "if user does not have a code: prompts user to request activation code", %{conn: conn} do
conn = get(conn, "/activate")
assert html_response(conn, 200) =~ "Request activation code"
end
test "sends the welcome email", %{conn: conn} do
token = Plausible.Auth.Token.sign_activation("Jane Doe", "user@example.com")
get(conn, "/claim-activation?token=#{token}")
test "if user does have a code: prompts user to enter the activation code from their email", %{conn: conn} do
conn = post(conn, "/activate/request-code")
|> get("/activate")
assert_email_delivered_with(subject: "Welcome to Plausible")
assert html_response(conn, 200) =~ "Please enter the 4-digit code we sent to"
end
end
describe "POST /activate/request-code" do
setup [:create_user, :log_in]
test "associates an activation pin with the user account", %{conn: conn, user: user} do
post(conn, "/activate/request-code")
code = Repo.one(from c in "email_verification_codes", where: c.user_id == ^user.id, select: %{user_id: c.user_id, issued_at: c.issued_at})
assert code[:user_id] == user.id
assert Timex.after?(code[:issued_at], Timex.now() |> Timex.shift(seconds: -10))
end
test "redirects new user to create a password", %{conn: conn} do
token = Plausible.Auth.Token.sign_activation("Jane Doe", "user@example.com")
conn = get(conn, "/claim-activation?token=#{token}")
test "sends activation email to user", %{conn: conn, user: user} do
post(conn, "/activate/request-code")
assert redirected_to(conn) == "/password"
assert_delivered_email_matches(%{to: [{_, user_email}], subject: subject})
assert user_email == user.email
assert subject =~ "is your Plausible email verification code"
end
end
describe "POST /activate" do
setup [:create_user, :log_in]
test "with wrong pin - reloads the form with error", %{conn: conn} do
conn = post(conn, "/activate", %{code: "1234"})
assert html_response(conn, 200) =~ "Incorrect activation code"
end
test "redirects existing user to create a password", %{conn: conn} do
token = Plausible.Auth.Token.sign_activation("Jane Doe", "user@example.com")
test "with expired pin - reloads the form with error", %{conn: conn, user: user} do
Repo.insert_all("email_verification_codes", [%{
code: 1234,
user_id: user.id,
issued_at: Timex.shift(Timex.now(), days: -1)
}])
conn = get(conn, "/claim-activation?token=#{token}")
conn = get(conn, "/claim-activation?token=#{token}")
conn = post(conn, "/activate", %{code: "1234"})
assert redirected_to(conn) == "/password"
assert html_response(conn, 200) =~ "Code is expired, please request another one"
end
test "marks the user account as active", %{conn: conn, user: user} do
Repo.update!(Plausible.Auth.User.changeset(user, %{email_verified: false}))
post(conn, "/activate/request-code")
code = Repo.one(from c in "email_verification_codes", where: c.user_id == ^user.id, select: c.code) |> Integer.to_string
conn = post(conn, "/activate", %{code: code})
user = Repo.get_by(Plausible.Auth.User, id: user.id)
assert user.email_verified
assert redirected_to(conn) == "/sites/new"
end
test "removes the user association from the verification code", %{conn: conn, user: user} do
Repo.update!(Plausible.Auth.User.changeset(user, %{email_verified: false}))
post(conn, "/activate/request-code")
code = Repo.one(from c in "email_verification_codes", where: c.user_id == ^user.id, select: c.code) |> Integer.to_string
post(conn, "/activate", %{code: code})
refute Repo.exists?(from c in "email_verification_codes", where: c.user_id == ^user.id)
end
end

View File

@ -14,7 +14,8 @@ defmodule Plausible.Factory do
name: "Jane Smith",
email: sequence(:email, &"email-#{&1}@example.com"),
password_hash: Plausible.Auth.Password.hash(pw),
trial_expiry_date: Timex.today() |> Timex.shift(days: 30)
trial_expiry_date: Timex.today() |> Timex.shift(days: 30),
email_verified: true
}
merge_attributes(user, attrs)

View File

@ -0,0 +1,28 @@
defmodule Plausible.Workers.CleanEmailVerificationCodesTest do
use Plausible.DataCase
alias Plausible.Workers.CleanEmailVerificationCodes
defp issue_code(user, issued_at) do
code = Repo.one(from(c in "email_verification_codes", where: is_nil(c.user_id), select: c.code, limit: 1))
Repo.update_all(from(c in "email_verification_codes", where: c.code == ^code), [set: [user_id: user.id, issued_at: issued_at]])
end
test "cleans codes that are more than 4 hours old" do
user = insert(:user)
issue_code(user, Timex.now() |> Timex.shift(hours: -5))
issue_code(user, Timex.now() |> Timex.shift(days: -5))
CleanEmailVerificationCodes.perform(nil, nil)
refute Repo.exists?(from c in "email_verification_codes", where: c.user_id == ^user.id)
end
test "does not clean code from 2 hours ago" do
user = insert(:user)
issue_code(user, Timex.now() |> Timex.shift(hours: -2))
CleanEmailVerificationCodes.perform(nil, nil)
assert Repo.exists?(from c in "email_verification_codes", where: c.user_id == ^user.id)
end
end