towerops/lib/towerops_web/graphql/resolvers/member.ex
Graham McIntire 146f5745cf
fix: resolve compilation errors, test failures, and credo issues
- Escape HEEx template braces in GraphQL/API docs with raw(~S[...])
- Fix test assertions for updated marketing copy and UI text
- Extract helper functions in GraphQL resolvers to reduce nesting depth
- Create shared ErrorHelpers module for API controllers
- Fix ETS race condition in brute force whitelist cache for async tests
- Fix property test generators to use ASCII instead of printable unicode
- Add alert_severity helper to site_live/show
- Update accounts fixtures for explicit user confirmation
2026-02-14 12:23:10 -06:00

88 lines
3.1 KiB
Elixir

defmodule ToweropsWeb.GraphQL.Resolvers.Member do
@moduledoc "GraphQL resolvers for member and invitation management."
alias Towerops.Organizations
alias Towerops.Organizations.Invitation
alias Towerops.Repo
def list(_parent, _args, %{context: %{organization_id: org_id}}) do
members = Organizations.list_organization_members(org_id)
{:ok, members}
end
def list(_parent, _args, _resolution), do: {:error, "Authentication required"}
def invite(_parent, %{email: email} = args, %{context: %{organization_id: org_id, user: user}}) do
role = Map.get(args, :role, "member")
case Organizations.create_invitation(%{
email: email,
role: role,
organization_id: org_id,
invited_by_id: user.id
}) do
{:ok, invitation} -> {:ok, invitation}
{:error, changeset} -> {:error, format_errors(changeset)}
end
end
def invite(_parent, _args, _resolution), do: {:error, "Authentication required"}
def cancel_invitation(_parent, %{id: id}, %{context: %{organization_id: org_id}}) do
with {:ok, invitation} <- fetch_org_invitation(id, org_id) do
case Repo.delete(invitation) do
{:ok, _} -> {:ok, %{success: true, message: "Invitation cancelled"}}
{:error, _} -> {:ok, %{success: false, message: "Could not cancel invitation"}}
end
end
end
def cancel_invitation(_parent, _args, _resolution), do: {:error, "Authentication required"}
def remove(_parent, %{id: user_id}, %{context: %{organization_id: org_id}}) do
case Organizations.remove_member(org_id, user_id) do
{:ok, _} -> {:ok, %{success: true, message: "Member removed"}}
{:error, :cannot_remove_owner} -> {:error, "Cannot remove owner"}
{:error, :not_found} -> {:error, "Member not found"}
end
end
def remove(_parent, _args, _resolution), do: {:error, "Authentication required"}
def update_role(_parent, %{id: user_id, role: role}, %{context: %{organization_id: org_id}}) do
case Organizations.update_member_role(org_id, user_id, role) do
{:ok, membership} ->
membership = Repo.preload(membership, :user)
{:ok, membership}
{:error, :cannot_change_owner_role} ->
{:error, "Cannot change owner role"}
{:error, :not_found} ->
{:error, "Member not found"}
{:error, changeset} ->
{:error, format_errors(changeset)}
end
end
def update_role(_parent, _args, _resolution), do: {:error, "Authentication required"}
defp fetch_org_invitation(id, org_id) do
case Repo.get(Invitation, id) do
nil -> {:error, "Invitation not found"}
%Invitation{organization_id: ^org_id} = invitation -> {:ok, invitation}
%Invitation{} -> {:error, "Invitation not found"}
end
end
defp format_errors(changeset) do
changeset
|> Ecto.Changeset.traverse_errors(fn {msg, opts} ->
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
|> Enum.map_join("; ", fn {field, messages} -> "#{field}: #{Enum.join(messages, ", ")}" end)
end
end