fix: CustomField policies, no system-actor fallback, guidelines
- Tests and UI pass actor for CustomField create/read/destroy; seeds use actor - Member required-custom-fields validation uses context.actor only (no fallback) - CODE_GUIDELINES: add rule forbidding system-actor fallbacks
This commit is contained in:
parent
250369d142
commit
21dbdbe366
10 changed files with 116 additions and 43 deletions
|
|
@ -471,11 +471,12 @@ defmodule Mv.Membership.Member do
|
|||
end
|
||||
end
|
||||
|
||||
# Validate required custom fields
|
||||
validate fn changeset, _ ->
|
||||
# Validate required custom fields (actor from validation context only; no fallback)
|
||||
validate fn changeset, context ->
|
||||
provided_values = provided_custom_field_values(changeset)
|
||||
actor = context.actor
|
||||
|
||||
case Mv.Membership.list_required_custom_fields() do
|
||||
case Mv.Membership.list_required_custom_fields(actor: actor) do
|
||||
{:ok, required_custom_fields} ->
|
||||
missing_fields = missing_required_fields(required_custom_fields, provided_values)
|
||||
|
||||
|
|
|
|||
|
|
@ -155,23 +155,31 @@ defmodule Mv.Membership do
|
|||
Lists only required custom fields.
|
||||
|
||||
This is an optimized version that filters at the database level instead of
|
||||
loading all custom fields and filtering in memory.
|
||||
loading all custom fields and filtering in memory. Requires an actor for
|
||||
authorization (CustomField read policy).
|
||||
|
||||
## Options
|
||||
|
||||
- `:actor` - Required. The actor for authorization (e.g. current user).
|
||||
All roles can read CustomField; actor must have a valid role.
|
||||
|
||||
## Returns
|
||||
|
||||
- `{:ok, required_custom_fields}` - List of required custom fields
|
||||
- `{:error, error}` - Error reading custom fields
|
||||
- `{:error, error}` - Error reading custom fields (e.g. Forbidden when no actor)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, required_fields} = Mv.Membership.list_required_custom_fields()
|
||||
iex> {:ok, required_fields} = Mv.Membership.list_required_custom_fields(actor: actor)
|
||||
iex> Enum.all?(required_fields, & &1.required)
|
||||
true
|
||||
"""
|
||||
def list_required_custom_fields do
|
||||
def list_required_custom_fields(opts \\ []) do
|
||||
actor = Keyword.get(opts, :actor)
|
||||
|
||||
Mv.Membership.CustomField
|
||||
|> Ash.Query.filter(expr(required == true))
|
||||
|> Ash.read(domain: __MODULE__)
|
||||
|> Ash.read(domain: __MODULE__, actor: actor)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -176,6 +176,13 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp stream_custom_fields(actor) do
|
||||
case Ash.read(Mv.Membership.CustomField, actor: actor) do
|
||||
{:ok, custom_fields} -> custom_fields
|
||||
{:error, _} -> []
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
# Track previous show_form state to detect when form is closed
|
||||
|
|
@ -207,7 +214,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
|||
|> assign_new(:show_delete_modal, fn -> false end)
|
||||
|> assign_new(:custom_field_to_delete, fn -> nil end)
|
||||
|> assign_new(:slug_confirmation, fn -> "" end)
|
||||
|> stream(:custom_fields, Ash.read!(Mv.Membership.CustomField), reset: true)}
|
||||
|> stream(:custom_fields, stream_custom_fields(assigns[:actor]), reset: true)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -226,7 +233,8 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
|||
|
||||
@impl true
|
||||
def handle_event("edit_custom_field", %{"id" => id}, socket) do
|
||||
custom_field = Ash.get!(Mv.Membership.CustomField, id)
|
||||
actor = socket.assigns[:actor]
|
||||
custom_field = Ash.get!(Mv.Membership.CustomField, id, actor: actor)
|
||||
|
||||
# Only send event if form was not already open
|
||||
if not socket.assigns[:show_form] do
|
||||
|
|
@ -242,7 +250,13 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
|||
|
||||
@impl true
|
||||
def handle_event("prepare_delete", %{"id" => id}, socket) do
|
||||
custom_field = Ash.get!(Mv.Membership.CustomField, id, load: [:assigned_members_count])
|
||||
actor = socket.assigns[:actor]
|
||||
|
||||
custom_field =
|
||||
Ash.get!(Mv.Membership.CustomField, id,
|
||||
load: [:assigned_members_count],
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|
|
@ -259,9 +273,10 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
|||
@impl true
|
||||
def handle_event("confirm_delete", _params, socket) do
|
||||
custom_field = socket.assigns.custom_field_to_delete
|
||||
actor = socket.assigns[:actor]
|
||||
|
||||
if socket.assigns.slug_confirmation == custom_field.slug do
|
||||
case Ash.destroy(custom_field) do
|
||||
case Ash.destroy(custom_field, actor: actor) do
|
||||
:ok ->
|
||||
send(self(), {:custom_field_deleted, custom_field})
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ defmodule MvWeb.GlobalSettingsLive do
|
|||
:if={@active_editing_section != :member_fields}
|
||||
module={MvWeb.CustomFieldLive.IndexComponent}
|
||||
id="custom-fields-component"
|
||||
actor={@current_user}
|
||||
/>
|
||||
</.form_section>
|
||||
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ defmodule MvWeb.MemberLive.Form do
|
|||
def mount(params, _session, socket) do
|
||||
# current_user should be set by on_mount hooks (LiveUserAuth + LiveHelpers)
|
||||
actor = current_actor(socket)
|
||||
{:ok, custom_fields} = Mv.Membership.list_custom_fields()
|
||||
{:ok, custom_fields} = Mv.Membership.list_custom_fields(actor: actor)
|
||||
|
||||
initial_custom_field_values =
|
||||
Enum.map(custom_fields, fn cf ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue