defmodule ToweropsWeb.Api.V1.InvitationsController do @moduledoc "API controller for organization invitations." use ToweropsWeb, :controller alias Towerops.Organizations def index(conn, _params) do organization_id = conn.assigns.current_organization_id invitations = organization_id |> Organizations.list_pending_invitations() |> Enum.map(&format_invitation/1) json(conn, %{data: invitations}) end def create(conn, %{"email" => email, "role" => role}) do organization_id = conn.assigns.current_organization_id user = conn.assigns[:current_user] attrs = %{ organization_id: organization_id, email: email, role: role, invited_by_id: user && user.id, token: Ecto.UUID.generate(), expires_at: DateTime.add(DateTime.utc_now(), 7 * 86_400, :second) |> DateTime.truncate(:second) } case Organizations.create_invitation(attrs) do {:ok, invitation} -> conn |> put_status(:created) |> json(%{data: format_invitation(Towerops.Repo.preload(invitation, :invited_by))}) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{errors: translate_errors(changeset)}) end end def create(conn, _params) do conn |> put_status(:bad_request) |> json(%{error: "Missing 'email' and 'role' parameters"}) end def delete(conn, %{"id" => id}) do organization_id = conn.assigns.current_organization_id invitation = Towerops.Repo.get!(Towerops.Organizations.Invitation, id) if invitation.organization_id == organization_id do case Organizations.delete_invitation(invitation) do {:ok, _} -> conn |> put_status(:no_content) |> send_resp(204, "") {:error, _} -> conn |> put_status(:unprocessable_entity) |> json(%{error: "Could not delete invitation"}) end else conn |> put_status(:not_found) |> json(%{error: "Invitation not found"}) end rescue Ecto.NoResultsError -> conn |> put_status(:not_found) |> json(%{error: "Invitation not found"}) end defp format_invitation(invitation) do %{ id: invitation.id, email: invitation.email, role: invitation.role, expires_at: invitation.expires_at, invited_by_email: get_in_loaded(invitation, :invited_by, :email), inserted_at: invitation.inserted_at } end defp get_in_loaded(struct, assoc, field) do case Map.get(struct, assoc) do %Ecto.Association.NotLoaded{} -> nil nil -> nil loaded -> Map.get(loaded, field) end end defp translate_errors(changeset) do Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> Regex.replace(~r"%{(\w+)}", msg, fn _, key -> if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() else key end end) end) end end