Packages

phoenix_kit

1.7.13
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 user_form.ex
Raw

lib/phoenix_kit_web/users/user_form.ex

defmodule PhoenixKitWeb.Users.UserForm do
@moduledoc """
LiveView for creating and editing users in the admin interface.
Provides a form for managing user data including email, password, roles,
and profile information. Supports both creation of new users and editing
existing users.
"""
use PhoenixKitWeb, :live_view
require Logger
alias PhoenixKit.Admin.Events
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth
alias PhoenixKit.Users.CustomFields
alias PhoenixKit.Users.Roles
alias PhoenixKit.Utils.IpAddress
alias PhoenixKit.Utils.Routes
def mount(params, _session, socket) do
user_id = params["id"]
mode = if user_id, do: :edit, else: :new
# Load custom field definitions
field_definitions = CustomFields.list_enabled_field_definitions()
# Load all roles for role management
all_roles = Roles.list_roles()
# Load default role for new user creation
default_role = Settings.get_setting("new_user_default_role", "User")
# Load timezone options
setting_options = Settings.get_setting_options()
timezone_options = [{"Use System Default", nil} | setting_options["time_zone"]]
socket =
socket
|> assign(:mode, mode)
|> assign(:user_id, user_id)
|> assign(:page_title, page_title(mode))
|> assign(:show_reset_password_modal, false)
|> assign(:show_password_field, false)
|> assign(:field_definitions, field_definitions)
|> assign(:custom_fields_errors, %{})
|> assign(:all_roles, all_roles)
|> assign(:pending_roles, [])
|> assign(:default_role, default_role)
|> assign(:timezone_options, timezone_options)
|> assign(:show_media_selector, false)
|> assign(:pending_avatar_file_id, nil)
|> assign(:avatar_changed, false)
|> load_user_data(mode, user_id)
|> load_form_data()
{:ok, socket}
end
def handle_event("open_media_selector", _params, socket) do
{:noreply, assign(socket, :show_media_selector, true)}
end
def handle_event("validate_user", %{"user" => user_params}, socket) do
# Filter password from params if password field is not shown
filtered_params =
if socket.assigns.mode == :edit and not socket.assigns.show_password_field do
Map.delete(user_params, "password")
else
user_params
end
# In edit mode, ensure username is preserved to prevent regeneration from email
filtered_params =
if socket.assigns.mode == :edit do
filtered_params = Map.put_new(filtered_params, "username", socket.assigns.user.username)
Logger.info(
"validate_user - user_params username: #{inspect(Map.get(user_params, "username"))}, filtered_params username: #{inspect(Map.get(filtered_params, "username"))}"
)
filtered_params
else
filtered_params
end
changeset =
case socket.assigns.mode do
:new ->
Auth.change_user_registration(%Auth.User{}, filtered_params)
:edit ->
Auth.User.profile_changeset(socket.assigns.user, filtered_params, validate_email: false)
end
|> Map.put(:action, :validate)
socket =
socket
|> assign(:changeset, changeset)
|> assign(:form_data, filtered_params)
|> assign(
:custom_fields_data,
Map.get(user_params, "custom_fields", socket.assigns.custom_fields_data)
)
{:noreply, socket}
end
def handle_event("save_user", %{"user" => user_params}, socket) do
case socket.assigns.mode do
:new -> create_user(socket, user_params)
:edit -> update_user(socket, user_params)
end
end
def handle_event("cancel", _params, socket) do
{:noreply, push_navigate(socket, to: Routes.path("/admin/users"))}
end
def handle_event("show_reset_password_modal", _params, socket) do
socket = assign(socket, :show_reset_password_modal, true)
{:noreply, socket}
end
def handle_event("hide_reset_password_modal", _params, socket) do
socket = assign(socket, :show_reset_password_modal, false)
{:noreply, socket}
end
def handle_event("admin_reset_password", _params, socket) do
user = socket.assigns.user
case Auth.deliver_user_reset_password_instructions(
user,
&Routes.url("/users/reset-password/#{&1}")
) do
{:ok, _} ->
socket =
socket
|> put_flash(
:info,
"Password reset email sent to #{user.email}. The user will receive instructions to reset their password."
)
|> assign(:show_reset_password_modal, false)
{:noreply, socket}
{:error, _reason} ->
socket =
put_flash(socket, :error, "Failed to send password reset email. Please try again.")
{:noreply, socket}
end
end
def handle_event("toggle_password_field", _params, socket) do
new_show_password_field = !socket.assigns.show_password_field
socket =
socket
|> assign(:show_password_field, new_show_password_field)
|> reload_changeset_with_password(new_show_password_field)
{:noreply, socket}
end
def handle_event("open_roles_dropdown", _params, socket) do
# Initialize pending roles with current user roles when dropdown opens
socket = assign(socket, :pending_roles, socket.assigns.user_roles)
{:noreply, socket}
end
def handle_event("toggle_role", %{"role" => role_name}, socket) do
# Toggle role in pending_roles list
pending_roles = socket.assigns.pending_roles
new_pending_roles =
if role_name in pending_roles do
List.delete(pending_roles, role_name)
else
[role_name | pending_roles]
end
socket = assign(socket, :pending_roles, new_pending_roles)
{:noreply, socket}
end
def handle_event("apply_roles", _params, socket) do
user = socket.assigns.user
current_user = socket.assigns.phoenix_kit_current_user
# Prevent users from modifying their own roles
if user.id == current_user.id do
socket =
put_flash(socket, :error, "You cannot modify your own roles.")
{:noreply, socket}
else
role_names = socket.assigns.pending_roles
case Roles.sync_user_roles(user, role_names) do
{:ok, _assignments} ->
socket =
socket
|> put_flash(:info, "User roles updated successfully.")
|> assign(:user_roles, Roles.get_user_roles(user))
|> assign(:pending_roles, [])
{:noreply, socket}
{:error, reason} ->
error_message =
case reason do
:owner_role_protected -> "Owner role cannot be manually assigned."
:cannot_remove_last_owner -> "Cannot remove Owner role from the last Owner."
_ -> "Failed to update roles. Please try again."
end
socket = put_flash(socket, :error, error_message)
{:noreply, socket}
end
end
end
def handle_event("sync_user_roles", params, socket) do
user = socket.assigns.user
current_user = socket.assigns.phoenix_kit_current_user
# Prevent users from modifying their own roles
if user.id == current_user.id do
socket =
put_flash(socket, :error, "You cannot modify your own roles.")
{:noreply, socket}
else
# Extract role names from form params
role_names =
params
|> Map.get("roles", %{})
|> Enum.filter(fn {_key, value} -> value != "false" end)
|> Enum.map(fn {key, _value} -> key end)
case Roles.sync_user_roles(user, role_names) do
{:ok, _assignments} ->
socket =
socket
|> put_flash(:info, "User roles updated successfully.")
|> assign(:user_roles, Roles.get_user_roles(user))
{:noreply, socket}
{:error, reason} ->
error_message =
case reason do
:owner_role_protected -> "Owner role cannot be manually assigned."
:cannot_remove_last_owner -> "Cannot remove Owner role from the last Owner."
_ -> "Failed to update roles. Please try again."
end
socket = put_flash(socket, :error, error_message)
{:noreply, socket}
end
end
end
def handle_event("toggle_user_status", _params, socket) do
user = socket.assigns.user
new_status = !user.is_active
case Auth.update_user_status(user, %{"is_active" => new_status}) do
{:ok, updated_user} ->
status_text = if new_status, do: "activated", else: "deactivated"
socket =
socket
|> put_flash(:info, "User #{status_text} successfully.")
|> assign(:user, updated_user)
|> reload_changeset_after_status_update(updated_user)
{:noreply, socket}
{:error, :cannot_deactivate_last_owner} ->
socket =
put_flash(
socket,
:error,
"Cannot deactivate the last Owner. Assign the Owner role to another user first."
)
{:noreply, socket}
{:error, _changeset} ->
socket =
put_flash(socket, :error, "Failed to update user status. Please try again.")
{:noreply, socket}
end
end
defp create_user(socket, user_params) do
ip_address = IpAddress.extract_from_socket(socket)
case Auth.register_user(user_params, ip_address) do
{:ok, user} ->
# Optionally send confirmation email
case Auth.deliver_user_confirmation_instructions(
user,
&Routes.url("/users/confirm/#{&1}")
) do
{:ok, _} -> :ok
# Continue even if email fails
{:error, _} -> :ok
end
socket =
socket
|> put_flash(:info, "User created successfully. Confirmation email sent.")
|> push_navigate(to: Routes.path("/admin/users"))
{:noreply, socket}
{:error, %Ecto.Changeset{} = changeset} ->
socket =
socket
|> assign(:changeset, changeset)
|> assign(:form_data, user_params)
{:noreply, socket}
end
end
defp update_user(socket, user_params) do
user = socket.assigns.user
custom_fields_params = Map.get(user_params, "custom_fields", %{})
# Include pending avatar if it was changed via media selector
custom_fields_params =
if socket.assigns[:avatar_changed] && socket.assigns[:pending_avatar_file_id] do
Map.put(custom_fields_params, "avatar_file_id", socket.assigns.pending_avatar_file_id)
else
custom_fields_params
end
profile_params = Map.delete(user_params, "custom_fields")
with :ok <- validate_custom_fields(user, custom_fields_params),
{:ok, updated_user} <- update_user_profile(socket, user, profile_params),
{:ok, user_with_fields} <- update_custom_fields(updated_user, custom_fields_params),
result <- update_user_roles_if_changed(socket, user_with_fields) do
# Clear avatar change flag after successful update
socket =
socket
|> assign(:avatar_changed, false)
|> assign(:pending_avatar_file_id, nil)
handle_update_result(socket, result)
else
{:error, %Ecto.Changeset{} = changeset} ->
handle_profile_update_error(socket, changeset, user_params)
{:error, :custom_fields, errors} ->
handle_custom_fields_error(socket, errors, custom_fields_params)
{:error, :custom_fields_save} ->
handle_custom_fields_save_error(socket)
end
end
defp validate_custom_fields(_user, custom_fields_params)
when map_size(custom_fields_params) == 0 do
:ok
end
defp validate_custom_fields(user, custom_fields_params) do
temp_user = %{user | custom_fields: custom_fields_params}
case CustomFields.validate_user_custom_fields(temp_user) do
:ok -> :ok
{:error, errors} -> {:error, :custom_fields, errors}
end
end
defp update_user_profile(socket, user, profile_params) do
password_provided =
socket.assigns.show_password_field &&
Map.has_key?(profile_params, "password") &&
profile_params["password"] != nil &&
String.trim(profile_params["password"]) != ""
if password_provided do
update_profile_and_password(socket, user, profile_params)
else
cleaned_params = Map.delete(profile_params, "password")
# Pass validate_email: false to skip uniqueness validation for username/email
# This allows users to keep their current username without it being regenerated
update_user_profile_without_validation(user, cleaned_params)
end
end
defp update_user_profile_without_validation(user, attrs) do
Logger.info(
"update_user_profile_without_validation - attrs username: #{inspect(Map.get(attrs, "username"))}"
)
changeset = Auth.User.profile_changeset(user, attrs, validate_email: false)
Logger.info(
"After profile_changeset, changeset changes username: #{inspect(Ecto.Changeset.get_change(changeset, :username))}, field: #{inspect(Ecto.Changeset.get_field(changeset, :username))}"
)
case changeset |> PhoenixKit.RepoHelper.repo().update() do
{:ok, updated_user} ->
Logger.info("After DB update, saved username: #{inspect(updated_user.username)}")
Events.broadcast_user_updated(updated_user)
{:ok, updated_user}
{:error, changeset} ->
{:error, changeset}
end
end
defp update_custom_fields(user, custom_fields_params)
when map_size(custom_fields_params) == 0 do
{:ok, user}
end
defp update_custom_fields(user, custom_fields_params) do
# Use update_user_fields instead of update_user_custom_fields to preserve existing fields (like avatar_file_id)
case Auth.update_user_fields(user, custom_fields_params) do
{:ok, updated_user} -> {:ok, updated_user}
{:error, _changeset} -> {:error, :custom_fields_save}
end
end
defp handle_update_result(socket, {:ok, _result}) do
# _result could be either the updated user or role assignments (from sync_user_roles)
# In both cases, we need to reload the user from the database to get the fresh data
user_id = socket.assigns.user.id
fresh_user = Auth.get_user!(user_id)
Logger.info(
"handle_update_result - fresh_user username from DB: #{inspect(fresh_user.username)}"
)
socket =
socket
|> assign(:user, fresh_user)
|> reload_changeset_with_updated_user(fresh_user)
|> put_flash(:info, "User updated successfully.")
|> push_navigate(to: Routes.path("/admin/users"))
{:noreply, socket}
end
defp handle_update_result(socket, {:error, reason}) do
error_message = format_role_update_error(reason)
socket =
socket
|> put_flash(:warning, error_message)
|> push_navigate(to: Routes.path("/admin/users"))
{:noreply, socket}
end
defp reload_changeset_with_updated_user(socket, updated_user) do
form_data = socket.assigns.form_data || %{}
# Ensure username is in form_data to prevent regeneration from email
form_data = Map.put_new(form_data, "username", updated_user.username)
# Use profile_changeset for edit mode to avoid username regeneration
changeset = Auth.User.profile_changeset(updated_user, form_data, validate_email: false)
assign(socket, :changeset, changeset)
end
defp format_role_update_error(:owner_role_protected) do
"User profile updated but Owner role cannot be manually assigned."
end
defp format_role_update_error(:cannot_remove_last_owner) do
"User profile updated but cannot remove Owner role from the last Owner."
end
defp format_role_update_error(_) do
"User profile updated but roles failed to update."
end
defp handle_profile_update_error(socket, changeset, user_params) do
socket =
socket
|> assign(:changeset, changeset)
|> assign(:form_data, user_params)
|> assign(:custom_fields_errors, %{})
{:noreply, socket}
end
defp handle_custom_fields_error(socket, errors, custom_fields_params) do
socket =
socket
|> assign(:custom_fields_errors, errors)
|> assign(:custom_fields_data, custom_fields_params)
|> put_flash(:error, "Please fix the custom field errors below.")
{:noreply, socket}
end
defp handle_custom_fields_save_error(socket) do
socket =
socket
|> put_flash(:error, "User profile updated but custom fields failed to save.")
|> push_navigate(to: Routes.path("/admin/users"))
{:noreply, socket}
end
defp load_user_data(socket, :new, _user_id) do
socket
|> assign(:user, %Auth.User{})
|> assign(:user_roles, [])
end
defp load_user_data(socket, :edit, user_id) do
user = Auth.get_user!(user_id)
user_roles = Roles.get_user_roles(user)
socket
|> assign(:user, user)
|> assign(:user_roles, user_roles)
|> assign(:pending_roles, user_roles)
end
defp load_form_data(%{assigns: %{mode: :new}} = socket) do
changeset = Auth.change_user_registration(%Auth.User{}, %{})
socket
|> assign(:changeset, changeset)
|> assign(:form_data, %{
"email" => "",
"password" => "",
"first_name" => "",
"last_name" => ""
})
|> assign(:custom_fields_data, %{})
end
defp load_form_data(%{assigns: %{mode: :edit, user: user}} = socket) do
# For edit mode, use profile_changeset instead of registration_changeset
# profile_changeset doesn't call maybe_generate_username_from_email like registration_changeset does
Logger.info("Loading form for user #{user.id}: DB username=#{inspect(user.username)}")
changeset =
Auth.User.profile_changeset(user, %{"username" => user.username}, validate_email: false)
Logger.info(
"After creating changeset, changeset changes: #{inspect(Ecto.Changeset.get_change(changeset, :username))}, changeset field: #{inspect(Ecto.Changeset.get_field(changeset, :username))}"
)
socket
|> assign(:changeset, changeset)
|> assign(:form_data, %{
"email" => user.email || "",
"username" => user.username || "",
"first_name" => user.first_name || "",
"last_name" => user.last_name || "",
"user_timezone" => user.user_timezone || "0"
})
|> assign(:custom_fields_data, user.custom_fields || %{})
end
defp page_title(:new), do: "Create User"
defp page_title(:edit), do: "Edit User"
defp reload_changeset_with_password(socket, show_password_field) do
case socket.assigns.mode do
:new ->
# For new users, password is always required
socket
:edit ->
# For edit mode, reload changeset to include/exclude password field
user = socket.assigns.user
form_data = socket.assigns.form_data || %{}
# Create changeset with or without password validation
changeset =
if show_password_field do
# Include password in changeset when field is shown
Auth.change_user_registration(user, form_data)
else
# Standard profile changeset when password field is hidden
Auth.change_user_registration(user, Map.delete(form_data, "password"))
end
assign(socket, :changeset, changeset)
end
end
defp reload_changeset_after_status_update(socket, updated_user) do
form_data = socket.assigns.form_data || %{}
changeset = Auth.change_user_registration(updated_user, form_data)
assign(socket, :changeset, changeset)
end
defp update_profile_and_password(socket, user, user_params) do
# First validate profile update
profile_params = Map.delete(user_params, "password")
case Auth.update_user_profile(user, profile_params) do
{:ok, updated_user} ->
# If profile update succeeded, update password
password_params = Map.take(user_params, ["password"])
# Build audit context from socket
context = build_audit_context(socket)
case Auth.admin_update_user_password(updated_user, password_params, context) do
{:ok, final_user} ->
{:ok, final_user}
{:error, password_changeset} ->
# If password update failed, return a combined changeset
profile_changeset = Auth.change_user_registration(user, user_params)
combined_changeset = merge_password_errors(profile_changeset, password_changeset)
{:error, combined_changeset}
end
{:error, profile_changeset} ->
# Profile update failed, return the profile changeset with password field
{:error, profile_changeset}
end
end
defp build_audit_context(socket) do
# Get admin user from socket assigns (set by on_mount)
admin_user = Map.get(socket.assigns, :phoenix_kit_current_user)
# Get IP address from socket metadata
ip_address =
case Phoenix.LiveView.get_connect_info(socket, :peer_data) do
%{address: address} -> :inet.ntoa(address) |> to_string()
_ -> nil
end
# Get user agent from socket metadata
user_agent =
case Phoenix.LiveView.get_connect_info(socket, :user_agent) do
ua when is_binary(ua) -> ua
_ -> nil
end
%{
admin_user: admin_user,
ip_address: ip_address,
user_agent: user_agent
}
end
defp merge_password_errors(profile_changeset, password_changeset) do
# Merge password errors into the profile changeset
password_errors = password_changeset.errors
Enum.reduce(password_errors, profile_changeset, fn {field, error}, acc ->
Ecto.Changeset.add_error(acc, field, elem(error, 0), elem(error, 1))
end)
end
defp update_user_roles_if_changed(socket, user) do
current_user = socket.assigns.phoenix_kit_current_user
pending_roles = socket.assigns.pending_roles
current_roles = socket.assigns.user_roles
# Check if roles have changed
roles_changed? = Enum.sort(pending_roles) != Enum.sort(current_roles)
cond do
# If user is trying to modify their own roles, prevent it
user.id == current_user.id and roles_changed? ->
{:error, :cannot_modify_own_roles}
# If roles haven't changed, skip update
not roles_changed? ->
{:ok, user}
# Roles have changed and it's allowed, update them
true ->
Roles.sync_user_roles(user, pending_roles)
end
end
def handle_info({:media_selected, file_ids}, socket) do
# Get the first selected file ID (single selection mode)
avatar_file_id = List.first(file_ids)
# Store avatar selection without saving to database yet
# Avatar will be saved when "Update User" button is clicked
socket =
if avatar_file_id do
Logger.info("Avatar selected (pending save): #{avatar_file_id}")
socket
|> assign(:pending_avatar_file_id, avatar_file_id)
|> assign(:avatar_changed, true)
|> assign(:show_media_selector, false)
|> put_flash(:info, "Avatar selected. Click 'Update User' to save.")
else
socket
|> assign(:show_media_selector, false)
end
{:noreply, socket}
end
def handle_info({:media_selector_closed}, socket) do
{:noreply, assign(socket, :show_media_selector, false)}
end
end