Add AssignDefaultRole change for automatic role assignment
- Assigns 'Mitglied' role to new users if no role is set
This commit is contained in:
parent
5c0786ebca
commit
4b10fd2702
1 changed files with 103 additions and 0 deletions
103
lib/accounts/user/changes/assign_default_role.ex
Normal file
103
lib/accounts/user/changes/assign_default_role.ex
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
defmodule Mv.Accounts.User.Changes.AssignDefaultRole do
|
||||||
|
@moduledoc """
|
||||||
|
Assigns the default "Mitglied" role to new users if no role is explicitly provided.
|
||||||
|
|
||||||
|
This change runs during user creation actions (`:create_user`, `:register_with_password`,
|
||||||
|
`:register_with_rauthy`) and ensures that all users have a role assigned.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- Skips assignment if `role_id` is already set in the changeset, data, or arguments
|
||||||
|
- Loads the "Mitglied" role without authorization (safe for system operations)
|
||||||
|
- Returns unchanged changeset if "Mitglied" role doesn't exist (test environments)
|
||||||
|
- Adds error to changeset on unexpected failures
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- Works with upserts: When combined with `upsert_fields`, only new users get the role
|
||||||
|
- Uses `authorize?: false` to avoid circular dependencies during user creation
|
||||||
|
- The "Mitglied" role must exist (created by seeds or migration)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
# Automatically assigns "Mitglied" role during user creation:
|
||||||
|
{:ok, user} =
|
||||||
|
User
|
||||||
|
|> Ash.Changeset.for_create(:create_user, %{email: "new@example.com"})
|
||||||
|
|> Ash.create()
|
||||||
|
|
||||||
|
# User now has "Mitglied" role assigned
|
||||||
|
{:ok, user_with_role} = Ash.load(user, :role)
|
||||||
|
assert user_with_role.role.name == "Mitglied"
|
||||||
|
|
||||||
|
# Skips assignment if role is already set:
|
||||||
|
{:ok, user} =
|
||||||
|
User
|
||||||
|
|> Ash.Changeset.for_create(:create_user, %{email: "admin@example.com", role_id: admin_role.id})
|
||||||
|
|> Ash.create()
|
||||||
|
|
||||||
|
# User has the explicitly set role, not "Mitglied"
|
||||||
|
{:ok, user_with_role} = Ash.load(user, :role)
|
||||||
|
assert user_with_role.role.name == "Admin"
|
||||||
|
"""
|
||||||
|
use Ash.Resource.Change
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
@spec change(Ash.Changeset.t(), keyword(), map()) :: Ash.Changeset.t()
|
||||||
|
def change(changeset, _opts, _context) do
|
||||||
|
# Check role_id in changeset attributes (for new assignments)
|
||||||
|
role_id_in_changeset = Ash.Changeset.get_attribute(changeset, :role_id)
|
||||||
|
|
||||||
|
# Check role_id in existing data (for upserts)
|
||||||
|
role_id_in_data = Map.get(changeset.data, :role_id)
|
||||||
|
|
||||||
|
# Check if role is being set via argument
|
||||||
|
role_arg = Ash.Changeset.get_argument(changeset, :role)
|
||||||
|
|
||||||
|
# Check if role relationship is already being managed
|
||||||
|
# Relationships are stored as a list of tuples: [{record_or_changes, opts}]
|
||||||
|
role_relationship = Map.get(changeset.relationships || %{}, :role)
|
||||||
|
|
||||||
|
# Skip if role is already set anywhere (changeset, data, argument, or relationship)
|
||||||
|
has_role =
|
||||||
|
not is_nil(role_id_in_changeset) or
|
||||||
|
not is_nil(role_id_in_data) or
|
||||||
|
not is_nil(role_arg) or
|
||||||
|
(not is_nil(role_relationship) and role_relationship != [])
|
||||||
|
|
||||||
|
if has_role do
|
||||||
|
changeset
|
||||||
|
else
|
||||||
|
assign_default_role(changeset)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec assign_default_role(Ash.Changeset.t()) :: Ash.Changeset.t()
|
||||||
|
defp assign_default_role(changeset) do
|
||||||
|
# Load the "Mitglied" role without authorization
|
||||||
|
# This is safe because:
|
||||||
|
# 1. We're only reading a public system role (no sensitive data)
|
||||||
|
# 2. This runs during user creation (bootstrap phase)
|
||||||
|
# 3. Using SystemActor here would create circular dependency (SystemActor needs a user)
|
||||||
|
case Mv.Authorization.Role.get_mitglied_role() do
|
||||||
|
{:ok, %Mv.Authorization.Role{} = mitglied_role} ->
|
||||||
|
# Assign the role using manage_relationship
|
||||||
|
# Note: :append_and_remove is the correct type for Ash 2.0+ (replaces :replace)
|
||||||
|
Ash.Changeset.manage_relationship(changeset, :role, mitglied_role,
|
||||||
|
type: :append_and_remove
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, nil} ->
|
||||||
|
# Role doesn't exist - skip assignment (common in test environments)
|
||||||
|
# In production, the migration will have created the role
|
||||||
|
changeset
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
# Unexpected error during role lookup
|
||||||
|
Ash.Changeset.add_error(changeset,
|
||||||
|
field: :role_id,
|
||||||
|
message: "Failed to load default role: #{inspect(error)}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue