From 7d33acde9fe6cfe83363f773bd84e3928beac097 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 27 Jan 2026 17:37:28 +0100 Subject: [PATCH] feat(system_actor): add system_user?/1 and normalize email Case-insensitive email comparison for system-actor detection. --- lib/mv/helpers/system_actor.ex | 32 ++++++++++++++++++++++++++- test/mv/helpers/system_actor_test.exs | 8 +++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/mv/helpers/system_actor.ex b/lib/mv/helpers/system_actor.ex index 565c2ef..8cd93d2 100644 --- a/lib/mv/helpers/system_actor.ex +++ b/lib/mv/helpers/system_actor.ex @@ -172,6 +172,31 @@ defmodule Mv.Helpers.SystemActor do end end + @doc """ + Returns whether the given user is the system actor user (case-insensitive email match). + + Use this instead of ad-hoc `to_string(user.email) == system_user_email()` so + comparisons are consistent and case-insensitive everywhere. + + ## Returns + + - `boolean()` - true if user's email matches system user email (case-insensitive) + + ## Examples + + iex> Mv.Helpers.SystemActor.system_user?(user_with_system_email) + true + iex> Mv.Helpers.SystemActor.system_user?(other_user) + false + + """ + @spec system_user?(Mv.Accounts.User.t() | map() | nil) :: boolean() + def system_user?(%{email: email}) when not is_nil(email) do + normalized_email(to_string(email)) == normalized_system_user_email() + end + + def system_user?(_), do: false + @doc """ Returns the email address of the system user. @@ -191,6 +216,11 @@ defmodule Mv.Helpers.SystemActor do @spec system_user_email() :: String.t() def system_user_email, do: system_user_email_config() + # Case-insensitive normalized form for comparisons + defp normalized_system_user_email, do: normalized_email(system_user_email_config()) + + defp normalized_email(email) when is_binary(email), do: String.downcase(email) + # Returns the system user email from environment variable or default # This allows configuration via SYSTEM_ACTOR_EMAIL env var @spec system_user_email_config() :: String.t() @@ -368,7 +398,7 @@ defmodule Mv.Helpers.SystemActor do upsert_identity: :unique_email, authorize?: false ) - |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.for_update(:update_internal, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.update!(authorize?: false) |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) diff --git a/test/mv/helpers/system_actor_test.exs b/test/mv/helpers/system_actor_test.exs index e676130..c2715ae 100644 --- a/test/mv/helpers/system_actor_test.exs +++ b/test/mv/helpers/system_actor_test.exs @@ -57,7 +57,7 @@ defmodule Mv.Helpers.SystemActorTest do |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do {:ok, user} when not is_nil(user) -> user - |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.for_update(:update_internal, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.update!(authorize?: false) |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) @@ -68,7 +68,7 @@ defmodule Mv.Helpers.SystemActorTest do upsert_identity: :unique_email, authorize?: false ) - |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.for_update(:update_internal, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) |> Ash.update!(authorize?: false) |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) @@ -373,9 +373,9 @@ defmodule Mv.Helpers.SystemActorTest do system_actor = SystemActor.get_system_actor() - # Assign wrong role to system user + # Assign wrong role to system user (use :update_internal so bootstrap-style update is allowed) system_user - |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.for_update(:update_internal, %{}) |> Ash.Changeset.manage_relationship(:role, read_only_role, type: :append_and_remove) |> Ash.update!(actor: system_actor)