Packages
phoenix_kit
1.7.227
1.7.236
1.7.235
1.7.234
1.7.233
1.7.232
1.7.231
1.7.230
1.7.229
1.7.228
1.7.227
1.7.226
1.7.225
1.7.224
1.7.223
1.7.222
1.7.221
1.7.220
1.7.219
1.7.218
1.7.217
1.7.216
1.7.215
1.7.214
1.7.213
1.7.212
1.7.211
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/users/magic_link_registration.ex
defmodule PhoenixKitWeb.Users.MagicLinkRegistration do
@moduledoc """
LiveView for magic link registration completion form.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Users.Auth
alias PhoenixKit.Users.Auth.User
alias PhoenixKit.Users.MagicLinkRegistration
alias PhoenixKit.Users.Referrals
alias PhoenixKit.Utils.Routes
alias PhoenixKitWeb.Users.Auth, as: WebAuth
@impl true
def mount(%{"token" => token}, _session, socket) do
case WebAuth.maybe_redirect_authenticated(socket) do
{:redirect, socket} ->
{:ok, socket}
:cont ->
# Gate the completion page too, not just the request page. Otherwise a
# link already sitting in an inbox still creates an account after an
# admin has switched magic-link registration off.
#
# BOTH switches, matching the request page. `allow_registration` is the
# broader one — "no new accounts at all" — so honouring only the
# magic-link-specific setting here left the wider switch as exactly the
# kind of button-hiding-without-route-closing this page was fixed for.
cond do
not PhoenixKit.Settings.get_boolean_setting("allow_registration", true) ->
# Registration is closed outright, so /users/register is no help.
{:ok,
socket
|> put_flash(:error, "Registration is currently disabled.")
|> redirect(to: Routes.path("/users/log-in"))}
not WebAuth.magic_link_registration_enabled?() ->
{:ok,
socket
|> put_flash(:error, "Magic link registration is currently disabled.")
|> redirect(to: Routes.path("/users/register"))}
true ->
mount_completion_form(token, socket)
end
end
end
defp mount_completion_form(token, socket) do
case MagicLinkRegistration.verify_registration_token(token) do
{:ok, email} ->
# Get referral codes configuration
referral_codes_config = Referrals.get_config()
# Generate username suggestion from email
suggested_username = User.generate_username_from_email(email)
changeset =
Auth.change_user_registration(%User{
email: email,
username: suggested_username
})
# Extract IP address for geolocation
ip_address =
case get_connect_info(socket, :peer_data) do
%{address: {a, b, c, d}} -> "#{a}.#{b}.#{c}.#{d}"
%{address: address} -> to_string(address)
_ -> "unknown"
end
{:ok,
socket
|> assign(:page_title, "Complete Registration")
|> assign(:token, token)
|> assign(:email, email)
|> assign(:ip_address, ip_address)
|> assign(:referral_codes_enabled, referral_codes_config.enabled)
|> assign(:referral_codes_required, referral_codes_config.required)
|> assign(:referral_code, nil)
|> assign(:referral_code_error, nil)
|> assign(:trigger_submit, false)
|> assign(:check_errors, false)
|> assign(:remember_me_available, WebAuth.remember_me_enabled?())
|> assign(:remember_me, WebAuth.remember_me_default?())
|> assign_form(changeset)}
{:error, _} ->
{:ok,
socket
|> put_flash(:error, "Registration link is invalid or has expired.")
|> redirect(to: Routes.path("/users/register"))}
end
end
@impl true
def handle_event("validate", %{"user" => user_params} = params, socket) do
referral_code = params["referral_code"]
user_params = form_params(user_params)
# Track the checkbox across re-renders so unticking it sticks.
socket = assign(socket, :remember_me, user_params["remember_me"] == "true")
case validate_referral_code(referral_code, socket, :change) do
{:ok, _} ->
socket =
socket
|> assign(referral_code: referral_code)
|> assign(referral_code_error: nil)
changeset =
%User{email: socket.assigns.email}
|> Auth.change_user_registration(user_params)
|> Map.put(:action, :validate)
{:noreply, assign_form(socket, changeset)}
{:error, error_message} ->
socket =
socket
|> assign(referral_code: referral_code)
|> assign(referral_code_error: error_message)
changeset =
%User{email: socket.assigns.email}
|> Auth.change_user_registration(user_params)
|> Map.put(:action, :validate)
{:noreply, assign_form(socket, changeset)}
end
end
@impl true
def handle_event("save", %{"user" => user_params} = params, socket) do
referral_code = params["referral_code"]
user_params = form_params(user_params)
case validate_referral_code(referral_code, socket, :submit) do
{:ok, _validated_code} ->
# Add referral_code to user params
user_params =
if referral_code && String.trim(referral_code) != "" do
Map.put(user_params, "referral_code", referral_code)
else
user_params
end
# Complete registration
case MagicLinkRegistration.complete_registration(
socket.assigns.token,
user_params,
socket.assigns.ip_address
) do
{:ok, user} ->
changeset = Auth.change_user_registration(user)
{:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
# `complete_registration/3` also returns bare atoms — `:invalid_token`
# once the token expires while the form sits open, and
# `:email_already_exists` if the address is claimed in between.
# Without these clauses either outcome was a CaseClauseError.
{:error, reason} ->
{:noreply,
socket
|> put_flash(:error, completion_error_message(reason))
|> redirect(to: Routes.path("/users/register/magic-link"))}
end
{:error, error_message} ->
socket =
socket
|> assign(referral_code: referral_code)
|> assign(referral_code_error: error_message)
|> assign(check_errors: true)
{:noreply, socket}
end
end
defp completion_error_message(:email_already_exists),
do: "That email is already registered. Please log in instead."
defp completion_error_message(_),
do: "Registration link is invalid or has expired. Please request a new one."
# Only what this form legitimately owns. `registration_changeset/3` also casts
# `custom_fields` and the geo columns — fine for server-side callers, but a
# phx-submit payload is whatever the client sends.
@form_fields ~w(email username password first_name last_name account_type
organization_name user_timezone remember_me return_to)
defp form_params(user_params) when is_map(user_params),
do: Map.take(user_params, @form_fields)
defp form_params(other), do: other
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
form = to_form(changeset, as: "user")
if changeset.valid? do
assign(socket, form: form, check_errors: false)
else
assign(socket, form: form)
end
end
# `context` is `:change` while the user types and `:submit` on the final
# attempt. See `PhoenixKit.Users.Referrals.validate_for_signup/2` for why the
# two differ and why every rejection reads the same.
defp validate_referral_code(referral_code, socket, context) do
Referrals.validate_for_signup(referral_code,
enabled?: socket.assigns.referral_codes_enabled,
required?: socket.assigns.referral_codes_required,
context: context,
ip_address: socket.assigns[:ip_address]
)
end
end