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:
Moritz 2026-01-29 13:53:55 +01:00 committed by moritz
parent 36b5d5880b
commit 1d17c4f2dd
10 changed files with 116 additions and 43 deletions

View file

@ -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})

View file

@ -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>

View file

@ -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 ->