analytics/lib/plausible/site/memberships/create_invitation.ex

145 lines
4.7 KiB
Elixir

defmodule Plausible.Site.Memberships.CreateInvitation do
@moduledoc """
Service for inviting new or existing users to a sites, including ownershhip
transfers.
"""
alias Plausible.Auth.{User, Invitation}
alias Plausible.{Site, Sites, Site.Membership}
alias Plausible.Site.Memberships.Invitations
alias Plausible.Billing.Quota
import Ecto.Query
@type invite_error() ::
Ecto.Changeset.t()
| :already_a_member
| :transfer_to_self
| :no_plan
| {:over_limit, non_neg_integer()}
| :forbidden
@spec create_invitation(Site.t(), User.t(), String.t(), atom()) ::
{:ok, Invitation.t()} | {:error, invite_error()}
@doc """
Invites a new team member to the given site. Returns a
%Plausible.Auth.Invitation{} struct and sends the invitee an email to accept
this invitation.
The inviter must have enough permissions to invite the new team member,
otherwise this function returns `{:error, :forbidden}`.
If the new team member role is `:owner`, this function handles the invitation
as an ownership transfer and requires the inviter to be the owner of the site.
"""
def create_invitation(site, inviter, invitee_email, role) do
Plausible.Repo.transaction(fn ->
do_invite(site, inviter, invitee_email, role)
end)
end
@spec bulk_transfer_ownership_direct([Site.t()], User.t()) ::
{:ok, [Membership.t()]}
| {:error,
invite_error()
| Quota.over_limits_error()}
def bulk_transfer_ownership_direct(sites, new_owner) do
Plausible.Repo.transaction(fn ->
for site <- sites do
site = Plausible.Repo.preload(site, :owner)
case Site.Memberships.transfer_ownership(site, new_owner) do
{:ok, membership} ->
membership
{:error, error} ->
Plausible.Repo.rollback(error)
end
end
end)
end
@spec bulk_create_invitation([Site.t()], User.t(), String.t(), atom(), Keyword.t()) ::
{:ok, [Invitation.t()]} | {:error, invite_error()}
def bulk_create_invitation(sites, inviter, invitee_email, role, opts \\ []) do
Plausible.Repo.transaction(fn ->
for site <- sites do
do_invite(site, inviter, invitee_email, role, opts)
end
end)
end
defp do_invite(site, inviter, invitee_email, role, opts \\ []) do
attrs = %{email: invitee_email, role: role, site_id: site.id, inviter_id: inviter.id}
with site <- Plausible.Repo.preload(site, :owner),
:ok <- check_invitation_permissions(site, inviter, role, opts),
:ok <- check_team_member_limit(site, role),
invitee <- Plausible.Auth.find_user_by(email: invitee_email),
:ok <- Invitations.ensure_transfer_valid(site, invitee, role),
:ok <- ensure_new_membership(site, invitee, role),
%Ecto.Changeset{} = changeset <- Invitation.new(attrs),
{:ok, invitation} <- Plausible.Repo.insert(changeset) do
send_invitation_email(invitation, invitee)
invitation
else
{:error, cause} -> Plausible.Repo.rollback(cause)
end
end
defp check_invitation_permissions(site, inviter, requested_role, opts) do
check_permissions? = Keyword.get(opts, :check_permissions, true)
if check_permissions? do
required_roles = if requested_role == :owner, do: [:owner], else: [:admin, :owner]
membership_query =
from(m in Membership,
where: m.user_id == ^inviter.id and m.site_id == ^site.id and m.role in ^required_roles
)
if Plausible.Repo.exists?(membership_query), do: :ok, else: {:error, :forbidden}
else
:ok
end
end
defp send_invitation_email(invitation, invitee) do
invitation = Plausible.Repo.preload(invitation, [:site, :inviter])
email =
case {invitee, invitation.role} do
{invitee, :owner} -> PlausibleWeb.Email.ownership_transfer_request(invitation, invitee)
{nil, _role} -> PlausibleWeb.Email.new_user_invitation(invitation)
{%User{}, _role} -> PlausibleWeb.Email.existing_user_invitation(invitation)
end
Plausible.Mailer.send(email)
end
defp ensure_new_membership(_site, _invitee, :owner) do
:ok
end
defp ensure_new_membership(site, invitee, _role) do
if invitee && Sites.is_member?(invitee.id, site) do
{:error, :already_a_member}
else
:ok
end
end
defp check_team_member_limit(_site, :owner) do
:ok
end
defp check_team_member_limit(site, _role) do
site = Plausible.Repo.preload(site, :owner)
limit = Quota.team_member_limit(site.owner)
usage = Quota.team_member_usage(site.owner)
if Quota.below_limit?(usage, limit),
do: :ok,
else: {:error, {:over_limit, limit}}
end
end