defmodule MvWeb.LiveHelpers do @moduledoc """ Shared LiveView lifecycle hooks and helper functions. ## on_mount Hooks - `:default` - Sets the user's locale from session (defaults to "de") - `:ensure_user_role_loaded` - Ensures current_user has role relationship loaded ## Usage Add to LiveView modules via: ```elixir on_mount {MvWeb.LiveHelpers, :default} on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} ``` """ import Phoenix.Component def on_mount(:default, _params, session, socket) do locale = session["locale"] || "de" Gettext.put_locale(locale) {:cont, socket} end def on_mount(:ensure_user_role_loaded, _params, _session, socket) do socket = ensure_user_role_loaded(socket) {:cont, socket} end defp ensure_user_role_loaded(socket) do if socket.assigns[:current_user] do user = socket.assigns.current_user user_with_role = load_user_role(user) assign(socket, :current_user, user_with_role) else socket end end defp load_user_role(user) do case Map.get(user, :role) do %Ash.NotLoaded{} -> load_role_safely(user) nil -> load_role_safely(user) _role -> user end end defp load_role_safely(user) do # Use self as actor for loading own role relationship opts = [domain: Mv.Accounts, actor: user] case Ash.load(user, :role, opts) do {:ok, loaded_user} -> loaded_user {:error, error} -> # Log warning if role loading fails - this can cause authorization issues require Logger Logger.warning("Failed to load role for user #{user.id}: #{inspect(error)}") user end end @doc """ Helper function to get the current actor (user) from socket assigns. Provides consistent access pattern across all LiveViews. Returns nil if no current_user is present. ## Examples actor = current_actor(socket) members = Membership.list_members!(actor: actor) """ @spec current_actor(Phoenix.LiveView.Socket.t()) :: Mv.Accounts.User.t() | nil def current_actor(socket) do socket.assigns[:current_user] || socket.assigns.current_user end @doc """ Converts an actor to Ash options list for authorization. Returns empty list if actor is nil. Delegates to `Mv.Helpers.ash_actor_opts/1` for consistency across the application. ## Examples opts = ash_actor_opts(actor) Ash.read(query, opts) """ @spec ash_actor_opts(Mv.Accounts.User.t() | nil) :: keyword() defdelegate ash_actor_opts(actor), to: Mv.Helpers @doc """ Submits an AshPhoenix form with consistent actor handling. This wrapper ensures that actor is always passed via `action_opts` in a consistent manner across all LiveViews. ## Examples case submit_form(form, params, actor) do {:ok, resource} -> # success {:error, form} -> # validation errors end """ @spec submit_form(AshPhoenix.Form.t(), map(), Mv.Accounts.User.t() | nil) :: {:ok, Ash.Resource.t()} | {:error, AshPhoenix.Form.t()} def submit_form(form, params, actor) do AshPhoenix.Form.submit(form, params: params, action_opts: ash_actor_opts(actor)) end end