mitgliederverwaltung/lib/mv_web/live_helpers.ex
Moritz b10b9c893c
feat: add CheckPagePermission plug for page-level authorization
- Plug checks PermissionSets page list; redirects unauthorized to profile or sign-in.
- Router: add plug to :browser pipeline; LiveHelpers: check_page_permission_on_params
  for client-side navigation (push_patch).
2026-01-30 00:00:31 +01:00

126 lines
3.8 KiB
Elixir

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
- `:check_page_permission_on_params` - Attaches handle_params hook to enforce page permission on client-side navigation (push_patch)
## Usage
Add to LiveView modules via:
```elixir
on_mount {MvWeb.LiveHelpers, :default}
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
on_mount {MvWeb.LiveHelpers, :check_page_permission_on_params}
```
"""
import Phoenix.Component
alias MvWeb.Plugs.CheckPagePermission
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
def on_mount(:check_page_permission_on_params, _params, _session, socket) do
{:cont,
Phoenix.LiveView.attach_hook(
socket,
:check_page_permission,
:handle_params,
&check_page_permission_handle_params/3
)}
end
defp check_page_permission_handle_params(_params, uri, socket) do
path = uri |> URI.parse() |> Map.get(:path, "/") || "/"
if CheckPagePermission.public_path?(path) do
{:cont, socket}
else
user = socket.assigns[:current_user]
host = uri |> URI.parse() |> Map.get(:host) || "localhost"
if CheckPagePermission.user_can_access_page?(user, path, router: MvWeb.Router, host: host) do
{:cont, socket}
else
redirect_to = CheckPagePermission.redirect_target_for_user(user)
socket =
socket
|> Phoenix.LiveView.put_flash(:error, "You don't have permission to access this page.")
|> Phoenix.LiveView.push_navigate(to: redirect_to)
{:halt, socket}
end
end
end
defp ensure_user_role_loaded(socket) do
user = socket.assigns[:current_user]
if user do
# Use centralized Actor helper to ensure role is loaded
user_with_role = Mv.Authorization.Actor.ensure_loaded(user)
assign(socket, :current_user, user_with_role)
else
socket
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