User form: persist role, member linking, Forbidden handling
- User resource: update_user accepts role_id, manage_relationship :member - user_live/form: touch role_id, params_with_member_if_unchanged to avoid unlink - Handle Forbidden in form, extract error message for display - user_policies_test and form_test coverage
This commit is contained in:
parent
5ed41555e9
commit
8ec4a07103
4 changed files with 196 additions and 8 deletions
|
|
@ -8,6 +8,9 @@ defmodule Mv.Accounts.User do
|
|||
extensions: [AshAuthentication],
|
||||
authorizers: [Ash.Policy.Authorizer]
|
||||
|
||||
require Ash.Query
|
||||
import Ash.Expr
|
||||
|
||||
postgres do
|
||||
table "users"
|
||||
repo Mv.Repo
|
||||
|
|
@ -146,9 +149,10 @@ defmodule Mv.Accounts.User do
|
|||
|
||||
update :update_user do
|
||||
description "Updates a user and manages the optional member relationship. To change an existing member link, first remove it (set member to nil), then add the new one."
|
||||
# Only accept email directly - member_id is NOT in accept list
|
||||
# This prevents direct foreign key manipulation, forcing use of manage_relationship
|
||||
accept [:email]
|
||||
|
||||
# Accept email and role_id (role_id only used by admins; policy restricts update_user to admins).
|
||||
# member_id is NOT in accept list - use argument :member for relationship management.
|
||||
accept [:email, :role_id]
|
||||
# Allow member to be passed as argument for relationship management
|
||||
argument :member, :map, allow_nil?: true
|
||||
|
||||
|
|
@ -387,6 +391,49 @@ defmodule Mv.Accounts.User do
|
|||
end
|
||||
end
|
||||
|
||||
# Last-admin: prevent the only admin from changing their role (at least one admin required).
|
||||
validate fn changeset, _context ->
|
||||
if Ash.Changeset.changing_attribute?(changeset, :role_id) do
|
||||
current_role_id = changeset.data.role_id
|
||||
|
||||
current_role =
|
||||
Mv.Authorization.Role
|
||||
|> Ash.get!(current_role_id, authorize?: false)
|
||||
|
||||
if current_role.permission_set_name != "admin" do
|
||||
:ok
|
||||
else
|
||||
admin_role_ids =
|
||||
Mv.Authorization.Role
|
||||
|> Ash.Query.for_read(:read)
|
||||
|> Ash.Query.filter(expr(permission_set_name == "admin"))
|
||||
|> Ash.read!(authorize?: false)
|
||||
|> Enum.map(& &1.id)
|
||||
|
||||
# Count only non-system users with admin role (system user is for internal ops)
|
||||
system_email = Mv.Helpers.SystemActor.system_user_email()
|
||||
|
||||
count =
|
||||
Mv.Accounts.User
|
||||
|> Ash.Query.for_read(:read)
|
||||
|> Ash.Query.filter(expr(role_id in ^admin_role_ids))
|
||||
|> Ash.Query.filter(expr(email != ^system_email))
|
||||
|> Ash.count!(authorize?: false)
|
||||
|
||||
if count <= 1 do
|
||||
{:error,
|
||||
field: :role_id, message: "At least one user must keep the Admin role."}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end,
|
||||
on: [:update],
|
||||
where: [action_is(:update_user)]
|
||||
|
||||
# Prevent modification of the system actor user (required for internal operations).
|
||||
# Block update/destroy on UI-exposed actions only; :update_internal is used by bootstrap/tests.
|
||||
validate fn changeset, _context ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue