Packages

phoenix_kit

1.7.233
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
phoenix_kit lib phoenix_kit_web users registration.ex
Raw

lib/phoenix_kit_web/users/registration.ex

defmodule PhoenixKitWeb.Users.Registration do
@moduledoc """
LiveView for user registration.
Provides a registration form for new users to create an account.
Supports referral codes and respects the allow_registration setting.
Tracks anonymous visitor sessions during registration.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Admin.Presence
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth
alias PhoenixKit.Users.Auth.User
alias PhoenixKit.Users.Invitations
alias PhoenixKit.Users.Referrals
alias PhoenixKit.Utils.Date, as: UtilsDate
alias PhoenixKit.Utils.IpAddress
alias PhoenixKit.Utils.Routes
alias PhoenixKitWeb.Users.Auth, as: WebAuth
def mount(params, session, socket) do
case WebAuth.maybe_redirect_authenticated(socket) do
{:redirect, socket} ->
{:ok, socket}
:cont ->
do_mount(params, session, socket)
end
end
defp do_mount(params, session, socket) do
# Check if registration is allowed
allow_registration = Settings.get_boolean_setting("allow_registration", true)
if allow_registration do
# Track anonymous visitor session
if connected?(socket) do
session_id = session["live_socket_id"] || generate_session_id()
Presence.track_anonymous(session_id, %{
connected_at: UtilsDate.utc_now(),
ip_address: IpAddress.extract_from_socket(socket),
user_agent: get_connect_info(socket, :user_agent),
current_page: Routes.path("/users/register")
})
end
# Get project title from settings (with Config fallback)
project_title = PhoenixKit.Settings.get_project_title()
# Get referral codes configuration
referral_codes_config = Referrals.get_config()
# Get Magic Link registration setting
magic_link_registration_enabled =
Settings.get_boolean_setting("magic_link_registration_enabled", true)
# Get username field visibility setting
show_username = Settings.get_setting("registration_show_username", "true") != "false"
# Get organization accounts setting
org_accounts_enabled =
Settings.get_boolean_setting("enable_organization_accounts", false)
changeset = Auth.change_user_registration(%User{})
# Extract and store IP address during mount for later use
ip_address = IpAddress.extract_from_socket(socket)
# Parse invitation token from URL params
invitation_token = Map.get(params, "invitation")
pending_invitation = load_pending_invitation(invitation_token)
# Support return_to query param for post-registration redirect
# (mirrors the login page; wins over the after_registration_path setting)
return_to = sanitize_return_to(params["return_to"])
socket =
socket
|> assign(trigger_submit: false, check_errors: false)
|> assign(username_edited: false)
|> assign(project_title: project_title)
|> 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(user_ip_address: ip_address)
|> assign(magic_link_registration_enabled: magic_link_registration_enabled)
|> assign(show_username: show_username)
|> assign(org_accounts_enabled: org_accounts_enabled)
|> assign(pending_invitation: pending_invitation)
|> assign(pending_invitation_token: invitation_token)
|> assign(return_to: return_to)
|> assign(remember_me_available: WebAuth.remember_me_enabled?())
|> assign(remember_me: WebAuth.remember_me_default?())
|> assign_form(changeset)
{:ok, socket, temporary_assigns: [form: nil]}
else
socket =
socket
|> put_flash(
:error,
"User registration is currently disabled. Please contact an administrator."
)
|> redirect(to: Routes.path("/users/log-in"))
{:ok, socket}
end
end
def handle_event("save", %{"user" => user_params} = params, socket) do
if Settings.get_boolean_setting("allow_registration", true) do
do_save(user_params, params, socket)
else
# Re-checked here, not just in mount/3. A LiveView socket outlives the
# page load that created it, so a form mounted while registration was open
# keeps a working submit endpoint for as long as the tab stays open —
# closing registration has to close the submit, not only the entrance.
{:noreply,
socket
|> put_flash(:error, "Registration is currently disabled.")
|> redirect(to: Routes.path("/users/log-in"))}
end
end
def handle_event("validate", %{"user" => user_params} = params, socket) do
referral_code = params["referral_code"]
user_params = form_params(user_params)
# Track whether the user owns the username field, then keep it in sync with
# the email while it's still auto-managed.
username_edited = username_manually_edited?(socket, params, user_params)
user_params = maybe_sync_username(user_params, username_edited)
# The form re-renders on every change, so the checkbox has to be driven by
# what the user actually left it at — otherwise unticking it would snap
# back to the site default on the next keystroke.
socket =
socket
|> assign(username_edited: username_edited)
|> assign(remember_me: user_params["remember_me"] == "true")
# Validate referral code and update error state
case validate_referral_code(referral_code, socket, :change) do
{:ok, _} ->
socket =
socket
|> assign(referral_code: referral_code)
|> assign(referral_code_error: nil)
changeset = Auth.change_user_registration(%User{}, user_params)
{:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
{:error, error_message} ->
socket =
socket
|> assign(referral_code: referral_code)
|> assign(referral_code_error: error_message)
changeset = Auth.change_user_registration(%User{}, user_params)
{:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
end
end
defp do_save(user_params, params, socket) do
referral_code = params["referral_code"]
user_params = form_params(user_params)
# If the username was never manually edited, let the schema (re)generate it
# from the final email rather than persisting a possibly-stale preview value.
user_params = maybe_sync_username(user_params, socket.assigns.username_edited)
# Validate referral code if system is enabled
case validate_referral_code(referral_code, socket, :submit) do
{:ok, validated_code} ->
# Check if geolocation tracking is enabled
track_geolocation = Settings.get_boolean_setting("track_registration_geolocation", false)
ip_address = socket.assigns.user_ip_address
# Use appropriate registration function based on geolocation setting
registration_result =
if track_geolocation do
Auth.register_user_with_geolocation(user_params, ip_address)
else
Auth.register_user(user_params, ip_address)
end
case registration_result do
{:ok, user} ->
# Records the use AND marks the account as having satisfied
# invite-only, so a code supplied here is not asked for again by
# the access gate.
Referrals.record_signup_use(user, validated_code)
# Store invitation UUID in custom_fields for auto-accept after email confirmation
if socket.assigns[:pending_invitation] do
Auth.update_user_fields(user, %{
"pending_invitation_uuid" => socket.assigns.pending_invitation.uuid
})
# That auto-accept only fires from `confirm_user/1`. With
# confirmation not required there is no confirmation step, so
# accept now or the invitation is never redeemed.
maybe_accept_invitation_without_confirmation(user)
end
case Auth.deliver_user_confirmation_instructions(
user,
&Routes.url("/users/confirm/#{&1}")
) do
{:ok, _} ->
# Email sent successfully
:ok
{:error, error} ->
# Log error but don't fail registration
require Logger
Logger.error("Failed to send confirmation email: #{inspect(error)}")
:ok
end
PhoenixKit.Activity.log(%{
action: "user.registered",
module: "users",
mode: "manual",
actor_uuid: user.uuid,
resource_type: "user",
resource_uuid: user.uuid,
metadata: %{"method" => "self_registration", "actor_role" => "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)}
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
# Determines whether the user has taken manual ownership of the username field.
#
# `phx-change` reports the touched field in `_target`. Typing a non-empty value
# into the username field claims it (auto-sync stops); clearing it releases
# ownership so auto-sync from email resumes. Any other field leaves the current
# ownership state untouched.
defp username_manually_edited?(socket, params, user_params) do
case params["_target"] do
["user", "username"] -> String.trim(user_params["username"] || "") != ""
_ -> socket.assigns.username_edited
end
end
# While the username is still auto-managed, blank it so the registration
# changeset regenerates it from the (possibly just-corrected) email. Once the
# user owns the field, their value is passed through unchanged.
defp maybe_sync_username(user_params, true = _username_edited), do: user_params
defp maybe_sync_username(user_params, false = _username_edited),
do: Map.put(user_params, "username", "")
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[:user_ip_address]
)
end
defp generate_session_id do
:crypto.strong_rand_bytes(16) |> Base.encode64()
end
# Only allow relative paths to prevent open redirect attacks
defp sanitize_return_to(path) do
if Routes.local_path?(path), do: path, else: nil
end
# Fields the public form is allowed to set. `registration_changeset/3` also
# casts `custom_fields` and the `registration_*` geo columns — legitimate for
# server-side callers (OAuth, the geolocation path) but attacker-controlled
# here, since a phx-submit payload is whatever the client sends. Writing
# `custom_fields` from the browser would let a visitor plant
# `pending_invitation_uuid` or an `oauth_avatar_url` that the admin user list
# renders as an <img src>.
@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 maybe_accept_invitation_without_confirmation(user) do
if Settings.get_boolean_setting("require_email_confirmation", true) do
:ok
else
user = Auth.get_user(user.uuid) || user
uuid = user.custom_fields && user.custom_fields["pending_invitation_uuid"]
case uuid && Invitations.accept_invitation_by_uuid(uuid, user) do
{:ok, _} ->
Auth.update_user_fields(user, %{"pending_invitation_uuid" => nil})
:ok
_ ->
:ok
end
end
end
defp load_pending_invitation(nil), do: nil
defp load_pending_invitation(token) when is_binary(token) do
case Invitations.get_by_token(token) do
{:ok, invitation} -> invitation
{:error, _} -> nil
end
end
end