Member Resource Policies closes #345 #346

Merged
moritz merged 33 commits from feature/345_member_policies_2 into main 2026-01-13 16:36:24 +01:00
4 changed files with 27 additions and 6 deletions
Showing only changes of commit 897677a782 - Show all commits

View file

@ -39,6 +39,7 @@ defmodule Mv.Membership.Member do
require Ash.Query require Ash.Query
import Ash.Expr import Ash.Expr
alias Mv.Helpers
require Logger require Logger
alias Mv.Membership.Helpers.VisibilityConfig alias Mv.Membership.Helpers.VisibilityConfig
@ -1217,7 +1218,7 @@ defmodule Mv.Membership.Member do
# Extracts custom field values from existing member data (update scenario) # Extracts custom field values from existing member data (update scenario)
defp extract_existing_values(member_data, changeset) do defp extract_existing_values(member_data, changeset) do
actor = Map.get(changeset.context, :actor) actor = Map.get(changeset.context, :actor)
opts = if actor, do: [actor: actor], else: [] opts = Helpers.ash_actor_opts(actor)
case Ash.load(member_data, :custom_field_values, opts) do case Ash.load(member_data, :custom_field_values, opts) do
{:ok, %{custom_field_values: existing_values}} -> {:ok, %{custom_field_values: existing_values}} ->

View file

@ -2,7 +2,17 @@ defmodule Mv.EmailSync.Loader do
@moduledoc """ @moduledoc """
Helper functions for loading linked records in email synchronization. Helper functions for loading linked records in email synchronization.
Centralizes the logic for retrieving related User/Member entities. Centralizes the logic for retrieving related User/Member entities.
## Authorization
This module runs systemically and accepts optional actor parameters.
When called from hooks/changes, actor is extracted from changeset context.
When called directly, actor should be provided for proper authorization.
All functions accept an optional `actor` parameter that is passed to Ash operations
to ensure proper authorization checks are performed.
""" """
alias Mv.Helpers
@doc """ @doc """
Loads the member linked to a user, returns nil if not linked or on error. Loads the member linked to a user, returns nil if not linked or on error.
@ -13,7 +23,7 @@ defmodule Mv.EmailSync.Loader do
def get_linked_member(%{member_id: nil}, _actor), do: nil def get_linked_member(%{member_id: nil}, _actor), do: nil
def get_linked_member(%{member_id: id}, actor) do def get_linked_member(%{member_id: id}, actor) do
opts = if actor, do: [actor: actor], else: [] opts = Helpers.ash_actor_opts(actor)
case Ash.get(Mv.Membership.Member, id, opts) do case Ash.get(Mv.Membership.Member, id, opts) do
{:ok, member} -> member {:ok, member} -> member
@ -27,7 +37,7 @@ defmodule Mv.EmailSync.Loader do
Accepts optional actor for authorization. Accepts optional actor for authorization.
""" """
def get_linked_user(member, actor \\ nil) do def get_linked_user(member, actor \\ nil) do
opts = if actor, do: [actor: actor], else: [] opts = Helpers.ash_actor_opts(actor)
case Ash.load(member, :user, opts) do case Ash.load(member, :user, opts) do
{:ok, %{user: user}} -> user {:ok, %{user: user}} -> user
@ -42,7 +52,7 @@ defmodule Mv.EmailSync.Loader do
Accepts optional actor for authorization. Accepts optional actor for authorization.
""" """
def load_linked_user!(member, actor \\ nil) do def load_linked_user!(member, actor \\ nil) do
opts = if actor, do: [actor: actor], else: [] opts = Helpers.ash_actor_opts(actor)
case Ash.load(member, :user, opts) do case Ash.load(member, :user, opts) do
{:ok, %{user: user}} when not is_nil(user) -> {:ok, user} {:ok, %{user: user}} when not is_nil(user) -> {:ok, user}

View file

@ -8,6 +8,7 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
This allows creating members with the same email as unlinked users. This allows creating members with the same email as unlinked users.
""" """
use Ash.Resource.Validation use Ash.Resource.Validation
alias Mv.Helpers
@doc """ @doc """
Validates email uniqueness across linked Member-User pairs. Validates email uniqueness across linked Member-User pairs.
@ -51,7 +52,7 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
|> Ash.Query.filter(email == ^email) |> Ash.Query.filter(email == ^email)
|> maybe_exclude_id(exclude_user_id) |> maybe_exclude_id(exclude_user_id)
opts = if actor, do: [actor: actor], else: [] opts = Helpers.ash_actor_opts(actor)
case Ash.read(query, opts) do case Ash.read(query, opts) do
{:ok, []} -> {:ok, []} ->
@ -69,7 +70,7 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
defp maybe_exclude_id(query, id), do: Ash.Query.filter(query, id != ^id) defp maybe_exclude_id(query, id), do: Ash.Query.filter(query, id != ^id)
defp get_linked_user_id(member_data, actor) do defp get_linked_user_id(member_data, actor) do
opts = if actor, do: [actor: actor], else: [] opts = Helpers.ash_actor_opts(actor)
case Ash.load(member_data, :user, opts) do case Ash.load(member_data, :user, opts) do
{:ok, %{user: %{id: id}}} -> id {:ok, %{user: %{id: id}}} -> id

View file

@ -28,6 +28,15 @@ defmodule Mv.MembershipFees.CycleGenerator do
Uses PostgreSQL advisory locks to prevent race conditions when generating Uses PostgreSQL advisory locks to prevent race conditions when generating
cycles for the same member concurrently. cycles for the same member concurrently.
## Authorization
This module runs systemically and accepts optional actor parameters.
When called from hooks/changes, actor is extracted from changeset context.
When called directly, actor should be provided for proper authorization.
All functions accept an optional `actor` parameter in the `opts` keyword list
that is passed to Ash operations to ensure proper authorization checks are performed.
## Examples ## Examples
# Generate cycles for a single member # Generate cycles for a single member