defmodule PhoenixKitWeb.Live.Users.Users do @moduledoc """ User management LiveView for PhoenixKit admin panel. Provides comprehensive user management including listing, search, role assignment, and status updates. """ use PhoenixKitWeb, :live_view use Gettext, backend: PhoenixKitWeb.Gettext alias PhoenixKit.Admin.Events alias PhoenixKit.Settings alias PhoenixKit.Users.Auth alias PhoenixKit.Users.Roles alias PhoenixKit.Utils.Date, as: UtilsDate @per_page 10 def mount(params, _session, socket) do # Set locale for LiveView process locale = params["locale"] || socket.assigns[:current_locale] || "en" Gettext.put_locale(PhoenixKitWeb.Gettext, locale) Process.put(:phoenix_kit_current_locale, locale) # Subscribe to user events for live updates if connected?(socket) do Events.subscribe_to_users() Events.subscribe_to_stats() end # Get project title from settings project_title = Settings.get_setting("project_title", "PhoenixKit") # Load date/time format settings once for performance optimization # Use batch cached call for maximum efficiency date_time_settings = Settings.get_settings_cached( ["date_format", "time_format", "time_zone"], %{ "date_format" => "Y-m-d", "time_format" => "H:i", "time_zone" => "0" } ) socket = socket |> assign(:page, 1) |> assign(:per_page, @per_page) |> assign(:search_query, "") |> assign(:filter_role, "all") |> assign(:show_role_modal, false) |> assign(:managing_user, nil) |> assign(:user_roles, []) |> assign(:all_roles, []) |> assign(:confirmation_modal, %{show: false}) |> assign(:page_title, "Users") |> assign(:project_title, project_title) |> assign(:date_time_settings, date_time_settings) |> assign(:current_locale, locale) |> load_users() |> load_stats() {:ok, socket} end def handle_params(%{"action" => "add"} = _params, _url, socket) do # Open user registration form for adding new user socket = assign(socket, :show_add_user_modal, true) {:noreply, socket} end def handle_params(_params, _url, socket) do # Default case - no action specified socket = assign(socket, :show_add_user_modal, false) {:noreply, socket} end def handle_event("search", %{"search" => search_query}, socket) do socket = socket |> assign(:search_query, search_query) |> assign(:page, 1) |> load_users() {:noreply, socket} end def handle_event("filter_by_role", %{"role" => role}, socket) do socket = socket |> assign(:filter_role, role) |> assign(:page, 1) |> load_users() {:noreply, socket} end def handle_event("change_page", %{"page" => page}, socket) do page = String.to_integer(page) socket = socket |> assign(:page, page) |> load_users() {:noreply, socket} end def handle_event("show_role_management", %{"user_id" => user_id}, socket) do user_id_int = String.to_integer(user_id) user = Auth.get_user!(user_id_int) current_user = socket.assigns.phoenix_kit_current_user # Prevent self-modification for critical operations if current_user.id == user.id do socket = put_flash(socket, :error, "Cannot modify your own roles") {:noreply, socket} else # Get fresh user with preloaded roles to ensure accurate state user_with_roles = Auth.get_user_with_roles(user_id_int) user_roles = Roles.get_user_roles(user_with_roles) all_roles = Roles.list_roles() socket = socket |> assign(:managing_user, user_with_roles) |> assign(:user_roles, user_roles) |> assign(:all_roles, all_roles) |> assign(:show_role_modal, true) {:noreply, socket} end end def handle_event("hide_role_management", _params, socket) do socket = socket |> assign(:managing_user, nil) |> assign(:user_roles, []) |> assign(:all_roles, []) |> assign(:show_role_modal, false) {:noreply, socket} end def handle_event("sync_user_roles", %{"roles" => selected_roles}, socket) do user = socket.assigns.managing_user role_names = Map.values(selected_roles) case Roles.sync_user_roles(user, role_names) do {:ok, _assignments} -> socket = socket |> put_flash(:info, "User roles updated successfully") |> assign(:show_role_modal, false) |> assign(:managing_user, nil) |> assign(:user_roles, []) |> assign(:all_roles, []) |> load_users() |> load_stats() {:noreply, socket} {:error, reason} -> error_msg = case reason do :cannot_remove_last_owner -> "Cannot remove the last system owner" :owner_role_protected -> "Owner role cannot be assigned manually" _ -> "Failed to update user roles" end # Refresh the modal data on error to show current state user_with_roles = Auth.get_user_with_roles(user.id) updated_user_roles = Roles.get_user_roles(user_with_roles) socket = socket |> put_flash(:error, error_msg) |> assign(:managing_user, user_with_roles) |> assign(:user_roles, updated_user_roles) {:noreply, socket} end end def handle_event("quick_toggle_role", %{"user_id" => user_id, "role_name" => role_name}, socket) do user = Auth.get_user!(user_id) current_user = socket.assigns.phoenix_kit_current_user # Prevent self-modification if current_user.id == user.id do socket = put_flash(socket, :error, "Cannot modify your own roles") {:noreply, socket} else handle_role_toggle_result(toggle_user_role(user, role_name), role_name, socket) end end # New events for confirmation modal def handle_event( "request_status_toggle", %{"user_id" => user_id, "is_active" => is_active}, socket ) do is_active_bool = is_active == "true" confirmation_modal = %{ show: true, title: "Confirm Status Change", message: "Are you sure you want to #{if is_active_bool, do: "deactivate", else: "activate"} this user?", button_text: if(is_active_bool, do: "Deactivate", else: "Activate"), action: "toggle_user_status", user_id: user_id } {:noreply, assign(socket, :confirmation_modal, confirmation_modal)} end def handle_event( "request_confirmation_toggle", %{"user_id" => user_id, "is_confirmed" => is_confirmed}, socket ) do is_confirmed_bool = is_confirmed == "true" confirmation_modal = %{ show: true, title: "Confirm Email Status Change", message: "Are you sure you want to #{if is_confirmed_bool, do: "unconfirm", else: "confirm"} this user's email?", button_text: if(is_confirmed_bool, do: "Unconfirm", else: "Confirm"), action: "toggle_user_confirmation", user_id: user_id } {:noreply, assign(socket, :confirmation_modal, confirmation_modal)} end def handle_event("cancel_confirmation", _params, socket) do {:noreply, assign(socket, :confirmation_modal, %{show: false})} end def handle_event("confirm_action", %{"action" => action, "user_id" => user_id}, socket) do # Close modal first socket = assign(socket, :confirmation_modal, %{show: false}) # Execute the confirmed action case action do "toggle_user_status" -> handle_toggle_user_status(%{"user_id" => user_id}, socket) "toggle_user_confirmation" -> handle_toggle_user_confirmation(%{"user_id" => user_id}, socket) _ -> {:noreply, socket} end end # Keep old handlers for backward compatibility, but make them delegate to private handlers def handle_event("toggle_user_status", %{"user_id" => user_id}, socket) do handle_toggle_user_status(%{"user_id" => user_id}, socket) end def handle_event("toggle_user_confirmation", %{"user_id" => user_id}, socket) do handle_toggle_user_confirmation(%{"user_id" => user_id}, socket) end # Keep the original handlers private for internal use defp handle_toggle_user_status(%{"user_id" => user_id}, socket) do current_user = socket.assigns.phoenix_kit_current_user user = Auth.get_user!(user_id) if current_user.id == user.id do socket = put_flash(socket, :error, "Cannot modify your own status") {:noreply, socket} else toggle_user_status_safely(socket, user) end end defp handle_toggle_user_confirmation(%{"user_id" => user_id}, socket) do current_user = socket.assigns.phoenix_kit_current_user user = Auth.get_user!(user_id) if current_user.id == user.id do socket = put_flash(socket, :error, "Cannot modify your own confirmation status") {:noreply, socket} else toggle_user_confirmation_safely(socket, user) end end defp toggle_user_status_safely(socket, user) do new_status = !user.is_active case Auth.update_user_status(user, %{"is_active" => new_status}) do {:ok, _user} -> status_text = if new_status, do: "activated", else: "deactivated" socket = socket |> put_flash(:info, "User #{status_text} successfully") |> load_users() |> load_stats() {:noreply, socket} {:error, :cannot_deactivate_last_owner} -> socket = put_flash(socket, :error, "Cannot deactivate the last system owner") {:noreply, socket} {:error, _changeset} -> socket = put_flash(socket, :error, "Failed to update user status") {:noreply, socket} end end defp toggle_user_confirmation_safely(socket, user) do case Auth.toggle_user_confirmation(user) do {:ok, updated_user} -> status_text = if updated_user.confirmed_at, do: "confirmed", else: "unconfirmed" socket = socket |> put_flash(:info, "User email #{status_text} successfully") |> load_users() |> load_stats() {:noreply, socket} {:error, _changeset} -> socket = put_flash(socket, :error, "Failed to update user confirmation status") {:noreply, socket} end end defp load_users(socket) do params = [ page: socket.assigns.page, page_size: socket.assigns.per_page, search: socket.assigns.search_query, role: socket.assigns.filter_role ] %{users: users, total_count: total_count, total_pages: total_pages} = Auth.list_users_paginated(params) # Force refresh by clearing users first, then setting new ones # This ensures LiveView doesn't reuse stale user objects with cached roles socket |> assign(:users, []) |> assign(:users, users) |> assign(:total_count, total_count) |> assign(:total_pages, total_pages) end defp load_stats(socket) do stats = Roles.get_extended_stats() socket |> assign(:total_users, stats.total_users) |> assign(:total_owners, stats.owner_count) |> assign(:total_admins, stats.admin_count) |> assign(:total_regular_users, stats.user_count) |> assign(:active_users, stats.active_users) |> assign(:inactive_users, stats.inactive_users) |> assign(:confirmed_users, stats.confirmed_users) |> assign(:pending_users, stats.pending_users) end defp get_user_roles(user) do # Use preloaded roles if available case Ecto.assoc_loaded?(user.roles) do true -> # Use preloaded roles directly since inactive assignments are deleted from DB user.roles |> Enum.map(& &1.name) |> Enum.sort() false -> # Fallback to DB query if roles not preloaded Roles.get_user_roles(user) end end defp user_has_role?(user, role_name) do role_name in get_user_roles(user) end defp toggle_user_role(user, role_name) do if user_has_role?(user, role_name) do case Roles.remove_role(user, role_name) do {:ok, _} -> {:ok, :removed} {:error, reason} -> {:error, reason} end else case Roles.assign_role(user, role_name) do {:ok, _} -> {:ok, :added} {:error, reason} -> {:error, reason} end end end defp handle_role_toggle_result(result, role_name, socket) do case result do {:ok, action} -> action_text = if action == :added, do: "added to", else: "removed from" socket = socket |> put_flash(:info, "Role #{role_name} #{action_text} user successfully") |> load_users() |> load_stats() {:noreply, socket} {:error, reason} -> error_msg = format_role_error_message(reason) socket = put_flash(socket, :error, error_msg) {:noreply, socket} end end defp format_role_error_message(reason) do case reason do :cannot_remove_last_owner -> "Cannot remove the last system owner" :owner_role_protected -> "Owner role cannot be assigned manually" :role_not_found -> "Role not found" _ -> "Failed to update user role" end end # Optimized function using preloaded roles - used in template conditionals def get_primary_role_name_unsafe(user) do roles = get_user_roles(user) cond do "Owner" in roles -> "Owner" "Admin" in roles -> "Admin" true -> "User" end end ## Live Event Handlers def handle_info({:user_created, _user}, socket) do socket = socket |> load_users() |> load_stats() {:noreply, socket} end def handle_info({:user_updated, _user}, socket) do socket = socket |> load_users() |> load_stats() {:noreply, socket} end def handle_info({:user_role_assigned, _user, _role_name}, socket) do socket = socket |> load_users() |> load_stats() {:noreply, socket} end def handle_info({:user_role_removed, _user, _role_name}, socket) do socket = socket |> load_users() |> load_stats() {:noreply, socket} end def handle_info({:user_roles_synced, _user, _new_roles}, socket) do socket = socket |> load_users() |> load_stats() {:noreply, socket} end def handle_info({:user_confirmed, _user}, socket) do socket = socket |> load_users() |> load_stats() {:noreply, socket} end def handle_info({:user_unconfirmed, _user}, socket) do socket = socket |> load_users() |> load_stats() {:noreply, socket} end def handle_info({:stats_updated, stats}, socket) do socket = socket |> assign(:total_users, stats.total_users) |> assign(:total_owners, stats.owner_count) |> assign(:total_admins, stats.admin_count) |> assign(:total_regular_users, stats.user_count) |> assign(:active_users, stats.active_users) |> assign(:inactive_users, stats.inactive_users) |> assign(:confirmed_users, stats.confirmed_users) |> assign(:pending_users, stats.pending_users) {:noreply, socket} end end