feat: add approval ui for join requests
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon 2026-03-11 02:04:03 +01:00
parent 50433e607f
commit 86d9242d83
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
22 changed files with 1624 additions and 12 deletions

View file

@ -40,6 +40,13 @@ defmodule Mv.Membership.JoinRequest do
change Mv.Membership.JoinRequest.Changes.FilterFormDataByAllowlist
end
# Internal/seeding only: create with status submitted (no policy allows; use authorize?: false).
create :create_submitted do
description "Create a join request with status submitted (seeds, internal use only)"
accept [:email, :first_name, :last_name, :form_data, :schema_version]
change Mv.Membership.JoinRequest.Changes.SetSubmittedForSeeding
end
read :get_by_confirmation_token_hash do
description "Find a join request by confirmation token hash (for confirm flow only)"
argument :confirmation_token_hash, :string, allow_nil?: false
@ -56,25 +63,64 @@ defmodule Mv.Membership.JoinRequest do
change Mv.Membership.JoinRequest.Changes.ConfirmRequest
end
update :approve do
description "Approve a submitted join request and promote to Member"
require_atomic? false
change Mv.Membership.JoinRequest.Changes.ApproveRequest
end
update :reject do
description "Reject a submitted join request"
require_atomic? false
change Mv.Membership.JoinRequest.Changes.RejectRequest
end
end
policies do
policy action(:submit) do
# Use :strict so unauthorized access returns Forbidden (not empty list).
# Default :filter would silently return [] for unauthorized reads instead of Forbidden.
default_access_type :strict
# Public actions: bypass so nil actor is immediately authorized (skips all remaining policies).
# Using bypass (not policy) avoids AND-combination with the read policy below.
bypass action(:submit) do
description "Allow unauthenticated submit (public join form)"
authorize_if Mv.Authorization.Checks.ActorIsNil
end
policy action(:get_by_confirmation_token_hash) do
bypass action(:get_by_confirmation_token_hash) do
description "Allow unauthenticated lookup by token hash for confirm"
authorize_if Mv.Authorization.Checks.ActorIsNil
end
policy action(:confirm) do
bypass action(:confirm) do
description "Allow unauthenticated confirm (confirmation link click)"
authorize_if Mv.Authorization.Checks.ActorIsNil
end
# Default read/destroy: no policy for actor nil → Forbidden
# READ: bypass for authorized roles (normal_user, admin).
# Uses a SimpleCheck (HasJoinRequestAccess) to avoid HasPermission.auto_filter returning
# expr(false), which would silently produce an empty list instead of Forbidden for
# unauthorized actors. See docs/policy-bypass-vs-haspermission.md.
# Unauthorized actors fall through to no matching policy → Ash default deny (Forbidden).
bypass action_type(:read) do
description "Allow normal_user and admin to read join requests (SimpleCheck bypass)"
authorize_if Mv.Authorization.Checks.HasJoinRequestAccess
end
# Approve/Reject: only actors with JoinRequest update permission
policy action(:approve) do
description "Allow authenticated users with JoinRequest update permission to approve"
authorize_if Mv.Authorization.Checks.HasPermission
end
policy action(:reject) do
description "Allow authenticated users with JoinRequest update permission to reject"
authorize_if Mv.Authorization.Checks.HasPermission
end
end
validations do
@ -135,6 +181,13 @@ defmodule Mv.Membership.JoinRequest do
update_timestamp :updated_at
end
relationships do
belongs_to :reviewed_by_user, Mv.Accounts.User do
define_attribute? false
source_attribute :reviewed_by_user_id
end
end
# Public helpers (used by SetConfirmationToken change and domain confirm_join_request)
@doc """