feat: implement attribute-level default for role_id assignment

Replace action-level changes with attribute default function to ensure
all users get the 'Mitglied' role regardless of creation path.
This commit is contained in:
Moritz 2026-01-25 13:39:10 +01:00 committed by Simon
parent 885fe613cb
commit 5164836d32
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2

View file

@ -68,12 +68,9 @@ defmodule Mv.Accounts.User do
hash_provider AshAuthentication.BcryptProvider
confirmation_required? false
# NOTE: The auto-generated :register_with_password action does NOT assign a default role.
# This is intentional because:
# - In production, users are created via OIDC (:register_with_rauthy), which DOES assign roles
# - Manual user creation via :create_user DOES assign roles
# - Tests that need a role can use :create_user or manually assign via fixtures
# - The migration ensures existing users without roles get the "Mitglied" role
resettable do
sender Mv.Accounts.User.Senders.SendPasswordResetEmail
end
end
end
end
@ -122,8 +119,7 @@ defmodule Mv.Accounts.User do
argument :member, :map, allow_nil?: true
upsert? true
# Assign default "Mitglied" role to new users
change Mv.Accounts.User.Changes.AssignDefaultRole
# Note: Default role is automatically assigned via attribute default (see attributes block)
# Manage the member relationship during user creation
change manage_relationship(:member, :member,
@ -273,9 +269,8 @@ defmodule Mv.Accounts.User do
# - The LinkOidcAccountLive will auto-link passwordless users without password prompt
validate Mv.Accounts.User.Validations.OidcEmailCollision
# Assign default "Mitglied" role to new OIDC users
# Note: upsert_fields [:email] ensures this doesn't overwrite existing users' roles
change Mv.Accounts.User.Changes.AssignDefaultRole
# Note: Default role is automatically assigned via attribute default (see attributes block)
# upsert_fields [:email] ensures existing users' roles are preserved during upserts
# Sync user email to member when linking (User → Member)
change Mv.EmailSync.Changes.SyncUserEmailToMember
@ -395,6 +390,15 @@ defmodule Mv.Accounts.User do
attribute :hashed_password, :string, sensitive?: true, allow_nil?: true
attribute :oidc_id, :string, allow_nil?: true
# Role assignment: Explicitly defined to enforce default value
# This ensures every user has a role, regardless of creation path
# (register_with_password, create_user, seeds, etc.)
attribute :role_id, :uuid do
allow_nil? false
default &__MODULE__.default_role_id/0
public? false
end
end
relationships do
@ -404,10 +408,13 @@ defmodule Mv.Accounts.User do
belongs_to :member, Mv.Membership.Member
# 1:1 relationship - User belongs to a Role
# This automatically creates a `role_id` attribute in the User table
# The relationship is optional (allow_nil? true by default)
# We define role_id ourselves (above in attributes) to control default value
# Foreign key constraint: on_delete: :restrict (prevents deleting roles assigned to users)
belongs_to :role, Mv.Authorization.Role
belongs_to :role, Mv.Authorization.Role do
define_attribute? false
source_attribute :role_id
allow_nil? false
end
end
identities do
@ -427,4 +434,29 @@ defmodule Mv.Accounts.User do
# forbid_if(always())
# end
# end
@doc """
Returns the default role ID for new users.
This function is called automatically when creating a user without an explicit role_id.
It fetches the "Mitglied" role from the database without authorization checks
(safe during user creation bootstrap phase).
## Returns
- UUID of the "Mitglied" role if it exists
- `nil` if the role doesn't exist (will cause validation error due to `allow_nil? false`)
## Examples
iex> Mv.Accounts.User.default_role_id()
"019bf2e2-873a-7712-a7ce-a5a1f90c5f4f"
"""
@spec default_role_id() :: Ecto.UUID.t() | nil
def default_role_id do
case Mv.Authorization.Role.get_mitglied_role() do
{:ok, %Mv.Authorization.Role{id: role_id}} -> role_id
_ -> nil
end
end
end