feat: add join request resource
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon 2026-03-09 14:44:45 +01:00
parent 2a04fad4fe
commit 2515a679b8
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
9 changed files with 323 additions and 5 deletions

View file

@ -9,6 +9,7 @@ defmodule Mv.Membership do
- `Setting` - Global application settings (singleton)
- `Group` - Groups that members can belong to
- `MemberGroup` - Join table for many-to-many relationship between Members and Groups
- `JoinRequest` - Public join form submissions (pending_confirmation submitted after email confirm)
## Public API
The domain exposes these main actions:
@ -27,6 +28,8 @@ defmodule Mv.Membership do
require Ash.Query
import Ash.Expr
alias Ash.Error.Query.NotFound, as: NotFoundError
alias Mv.Membership.JoinRequest
admin do
show? true
@ -80,6 +83,10 @@ defmodule Mv.Membership do
define :list_member_groups, action: :read
define :destroy_member_group, action: :destroy
end
resource Mv.Membership.JoinRequest do
define :submit_join_request, action: :submit
end
end
# Singleton pattern: Get the single settings record
@ -342,4 +349,49 @@ defmodule Mv.Membership do
|> Keyword.put_new(:domain, __MODULE__)
|> then(&Ash.read_one(query, &1))
end
@doc """
Confirms a join request by token (public confirmation link).
Hashes the token, finds the JoinRequest by confirmation_token_hash, then updates
to status :submitted and invalidates the token. Idempotent: if already submitted,
returns the existing record without changing it.
## Options
- `:actor` - Must be nil for public confirm (policy allows only unauthenticated).
## Returns
- `{:ok, request}` - Updated or already-submitted JoinRequest
- `{:error, error}` - Token unknown/invalid or authorization error
"""
def confirm_join_request(token, opts \\ []) when is_binary(token) do
hash = confirmation_token_hash(token)
actor = Keyword.get(opts, :actor)
query =
Ash.Query.for_read(JoinRequest, :get_by_confirmation_token_hash, %{
confirmation_token_hash: hash
})
case Ash.read_one(query, actor: actor, domain: __MODULE__) do
{:ok, nil} ->
{:error, NotFoundError.exception(resource: JoinRequest)}
{:ok, request} ->
if request.status == :submitted do
{:ok, request}
else
request
|> Ash.Changeset.for_update(:confirm, %{}, domain: __MODULE__)
|> Ash.update(domain: __MODULE__, actor: actor)
end
{:error, error} ->
{:error, error}
end
end
defp confirmation_token_hash(token) do
:crypto.hash(:sha256, token) |> Base.encode16(case: :lower)
end
end