analytics/lib/plausible_web/live/register_form.ex

331 lines
10 KiB
Elixir

defmodule PlausibleWeb.Live.RegisterForm do
@moduledoc """
LiveView for registration form.
"""
use PlausibleWeb, :live_view
use Phoenix.HTML
import PlausibleWeb.Live.Components.Form
alias Plausible.Auth
alias Plausible.Repo
def mount(params, _session, socket) do
socket =
assign_new(socket, :invitation, fn ->
if invitation_id = params["invitation_id"] do
Repo.get_by(Auth.Invitation, invitation_id: invitation_id)
end
end)
if socket.assigns.live_action == :register_from_invitation_form and
socket.assigns.invitation == nil do
{:ok, assign(socket, invitation_expired: true)}
else
changeset =
if invitation = socket.assigns.invitation do
Auth.User.settings_changeset(%Auth.User{email: invitation.email})
else
Auth.User.settings_changeset(%Auth.User{})
end
{:ok,
assign(socket,
form: to_form(changeset),
captcha_error: nil,
password_strength: Auth.User.password_strength(changeset),
trigger_submit: false
)}
end
end
def render(%{invitation_expired: true} = assigns) do
~H"""
<div class="mx-auto mt-6 text-center dark:text-gray-300">
<h1 class="text-3xl font-black">Plausible Analytics</h1>
<div class="text-xl font-medium">Lightweight and privacy-friendly web analytics</div>
</div>
<div class="w-full max-w-md mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mb-4 mt-8">
<h2 class="text-xl font-black dark:text-gray-100">Invitation expired</h2>
<p class="mt-4 text-sm">
Your invitation has expired or been revoked. Please request fresh one or you can <%= link(
"sign up",
class: "text-indigo-600 hover:text-indigo-900",
to: Routes.auth_path(@socket, :register)
) %> for a 30-day unlimited free trial without an invitation.
</p>
</div>
"""
end
def render(assigns) do
~H"""
<div class="mx-auto mt-6 text-center dark:text-gray-300">
<h1 class="text-3xl font-black">
<%= if ce?() or @live_action == :register_from_invitation_form do %>
Register your Plausible Analytics account
<% else %>
Register your 30-day free trial
<% end %>
</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 flex-shrink-0">
<.form
:let={f}
for={@form}
id="register-form"
phx-hook="Metrics"
phx-change="validate"
phx-submit="register"
phx-trigger-action={@trigger_submit}
class="w-full max-w-md mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mb-4 mt-8"
>
<input name="_csrf_token" type="hidden" value={Plug.CSRFProtection.get_csrf_token()} />
<h2 class="text-xl font-black dark:text-gray-100">Enter your details</h2>
<%= if @invitation do %>
<.email_input field={f[:email]} for_invitation={true} />
<.name_input field={f[:name]} />
<% else %>
<.name_input field={f[:name]} />
<.email_input field={f[:email]} for_invitation={false} />
<% end %>
<div class="my-4">
<div class="flex justify-between">
<label
for={f[:password].name}
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Password
</label>
<.password_length_hint minimum={12} field={f[:password]} />
</div>
<div class="mt-1">
<.password_input_with_strength
field={f[:password]}
strength={@password_strength}
phx-debounce={200}
class="dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300"
/>
</div>
</div>
<div class="my-4">
<label
for={f[:password_confirmation].name}
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Password confirmation
</label>
<div class="mt-1">
<.input
type="password"
autocomplete="new-password"
field={f[:password_confirmation]}
phx-debounce={200}
class="dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300"
/>
</div>
</div>
<%= if PlausibleWeb.Captcha.enabled?() do %>
<div class="mt-4">
<div
phx-update="ignore"
id="hcaptcha-placeholder"
class="h-captcha"
data-sitekey={PlausibleWeb.Captcha.sitekey()}
>
</div>
<%= if @captcha_error do %>
<div class="text-red-500 text-xs italic mt-3"><%= @captcha_error %></div>
<% end %>
<script
phx-update="ignore"
id="hcaptcha-script"
src="https://hcaptcha.com/1/api.js"
async
defer
>
</script>
</div>
<% end %>
<% submit_text =
if ce?() or @invitation do
"Create my account →"
else
"Start my free trial →"
end %>
<PlausibleWeb.Components.Generic.button id="register" type="submit" class="mt-4 w-full">
<%= submit_text %>
</PlausibleWeb.Components.Generic.button>
<p class="text-center text-gray-600 dark:text-gray-500 text-xs mt-4">
Already have an account? <%= link("Log in",
to: "/login",
class: "underline text-gray-800 dark:text-gray-50"
) %> instead.
</p>
</.form>
<div :if={@live_action == :register_form} class="pt-12 pl-8 hidden md:block">
<%= PlausibleWeb.AuthView.render("_onboarding_steps.html", current_step: 0) %>
</div>
</div>
"""
end
defp name_input(assigns) do
~H"""
<div class="my-4">
<label for={@field.name} class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Full name
</label>
<div class="mt-1">
<.input
field={@field}
placeholder="Jane Doe"
phx-debounce={200}
class="dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300"
/>
</div>
</div>
"""
end
defp email_input(assigns) do
email_classes = ~w(
dark:bg-gray-900
shadow-sm
focus:ring-indigo-500
focus:border-indigo-500
block
w-full
sm:text-sm
border-gray-300
dark:border-gray-500
rounded-md
dark:text-gray-300
)
{email_readonly, email_extra_classes} =
if assigns[:for_invitation] do
{[readonly: "readonly"], ["bg-gray-100"]}
else
{[], []}
end
assigns =
assigns
|> assign(:email_readonly, email_readonly)
|> assign(:email_classes, email_classes ++ email_extra_classes)
~H"""
<div class="my-4">
<div class="flex justify-between">
<label for={@field.name} class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Email
</label>
<p class="text-xs text-gray-500 mt-1">No spam, guaranteed.</p>
</div>
<div class="mt-1">
<.input
type="email"
field={@field}
placeholder="example@email.com"
phx-debounce={200}
class={@email_classes}
{@email_readonly}
/>
</div>
</div>
"""
end
def handle_event("validate", %{"user" => params}, socket) do
changeset =
params
|> Auth.User.new()
|> Map.put(:action, :validate)
password_strength = Auth.User.password_strength(changeset)
{:noreply, assign(socket, form: to_form(changeset), password_strength: password_strength)}
end
def handle_event(
"register",
%{"user" => _} = params,
%{assigns: %{invitation: %{} = invitation}} = socket
) do
if not PlausibleWeb.Captcha.enabled?() or
PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do
user =
params["user"]
|> Map.put("email", invitation.email)
|> Auth.User.new()
user =
case invitation.role do
:owner -> user
_ -> Plausible.Auth.User.remove_trial_expiry(user)
end
add_user(socket, user)
else
{:noreply, assign(socket, :captcha_error, "Please complete the captcha to register")}
end
end
def handle_event("register", %{"user" => _} = params, socket) do
if not PlausibleWeb.Captcha.enabled?() or
PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do
user = Auth.User.new(params["user"])
add_user(socket, user)
else
{:noreply, assign(socket, :captcha_error, "Please complete the captcha to register")}
end
end
def handle_event("send-metrics-after", _params, socket) do
{:noreply, assign(socket, trigger_submit: true)}
end
defp add_user(socket, user) do
case Repo.insert(user) do
{:ok, _user} ->
on_ee do
metrics_params =
if socket.assigns.invitation do
%{
event_name: "Signup via invitation",
params: %{
url:
Path.join(PlausibleWeb.Endpoint.url(), "/register/invitation/:invitation_id")
}
}
else
%{event_name: "Signup", params: %{}}
end
{:noreply, push_event(socket, "send-metrics", metrics_params)}
else
{:noreply, socket}
end
{:error, changeset} ->
{:noreply,
assign(socket,
form: to_form(Map.put(changeset, :action, :validate))
)}
end
end
end