mirror of
https://github.com/plausible/analytics.git
synced 2024-12-24 01:54:34 +03:00
Check for limits and features used by site before transferring ownership (#3445)
* Simplify team_member_usage query * Check limits before transferring ownership * Extract invite creation to dedicated service module * Simplify team member usage query * Remove unnecessary distinct clause * Delegate CreateInvitation via Memberships
This commit is contained in:
parent
a226773da1
commit
ad05af08a4
@ -108,27 +108,34 @@ defmodule Plausible.Billing.Quota do
|
|||||||
with the user's sites.
|
with the user's sites.
|
||||||
"""
|
"""
|
||||||
def team_member_usage(user) do
|
def team_member_usage(user) do
|
||||||
|
Plausible.Repo.aggregate(team_member_usage_query(user), :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def team_member_usage_query(user, site \\ nil) do
|
||||||
|
owned_sites_query = owned_sites_query(user)
|
||||||
|
|
||||||
|
owned_sites_query =
|
||||||
|
if site do
|
||||||
|
where(owned_sites_query, [os], os.site_id == ^site.id)
|
||||||
|
else
|
||||||
|
owned_sites_query
|
||||||
|
end
|
||||||
|
|
||||||
team_members_query =
|
team_members_query =
|
||||||
from os in subquery(owned_sites_query(user)),
|
from os in subquery(owned_sites_query),
|
||||||
inner_join: sm in Plausible.Site.Membership,
|
inner_join: sm in Plausible.Site.Membership,
|
||||||
on: sm.site_id == os.site_id,
|
on: sm.site_id == os.site_id,
|
||||||
inner_join: u in assoc(sm, :user),
|
inner_join: u in assoc(sm, :user),
|
||||||
select: %{email: u.email}
|
where: sm.role != :owner,
|
||||||
|
select: u.email
|
||||||
|
|
||||||
invitations_and_team_members_query =
|
from i in Plausible.Auth.Invitation,
|
||||||
from i in Plausible.Auth.Invitation,
|
inner_join: os in subquery(owned_sites_query),
|
||||||
inner_join: os in subquery(owned_sites_query(user)),
|
on: i.site_id == os.site_id,
|
||||||
on: i.site_id == os.site_id,
|
where: i.role != :owner,
|
||||||
where: i.role != :owner,
|
select: i.email,
|
||||||
select: %{email: i.email},
|
union: ^team_members_query
|
||||||
union: ^team_members_query
|
|
||||||
|
|
||||||
query =
|
|
||||||
from itm in subquery(invitations_and_team_members_query),
|
|
||||||
where: itm.email != ^user.email,
|
|
||||||
select: count(itm.email, :distinct)
|
|
||||||
|
|
||||||
Plausible.Repo.one(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec features_usage(Plausible.Auth.User.t()) :: [atom()]
|
@spec features_usage(Plausible.Auth.User.t()) :: [atom()]
|
||||||
|
@ -82,7 +82,11 @@ defmodule Plausible.SiteAdmin do
|
|||||||
|
|
||||||
if new_owner do
|
if new_owner do
|
||||||
{:ok, _} =
|
{:ok, _} =
|
||||||
Plausible.Sites.bulk_transfer_ownership(sites, inviter, new_owner.email,
|
Plausible.Site.Memberships.bulk_create_invitation(
|
||||||
|
sites,
|
||||||
|
inviter,
|
||||||
|
new_owner.email,
|
||||||
|
:owner,
|
||||||
check_permissions: false
|
check_permissions: false
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -100,7 +104,7 @@ defmodule Plausible.SiteAdmin do
|
|||||||
new_owner = Plausible.Auth.find_user_by(email: email)
|
new_owner = Plausible.Auth.find_user_by(email: email)
|
||||||
|
|
||||||
if new_owner do
|
if new_owner do
|
||||||
case Plausible.Sites.bulk_transfer_ownership_direct(sites, new_owner) do
|
case Plausible.Site.Memberships.bulk_transfer_ownership_direct(sites, new_owner) do
|
||||||
{:ok, _} -> :ok
|
{:ok, _} -> :ok
|
||||||
{:error, :transfer_to_self} -> {:error, "User is already an owner of one of the sites"}
|
{:error, :transfer_to_self} -> {:error, "User is already an owner of one of the sites"}
|
||||||
end
|
end
|
||||||
|
@ -14,6 +14,14 @@ defmodule Plausible.Site.Memberships do
|
|||||||
defdelegate reject_invitation(invitation_id, user), to: Memberships.RejectInvitation
|
defdelegate reject_invitation(invitation_id, user), to: Memberships.RejectInvitation
|
||||||
defdelegate remove_invitation(invitation_id, site), to: Memberships.RemoveInvitation
|
defdelegate remove_invitation(invitation_id, site), to: Memberships.RemoveInvitation
|
||||||
|
|
||||||
|
defdelegate create_invitation(site, inviter, invitee_email, role),
|
||||||
|
to: Memberships.CreateInvitation
|
||||||
|
|
||||||
|
defdelegate bulk_create_invitation(sites, inviter, invitee_email, role, opts),
|
||||||
|
to: Memberships.CreateInvitation
|
||||||
|
|
||||||
|
defdelegate bulk_transfer_ownership_direct(sites, new_owner), to: Memberships.CreateInvitation
|
||||||
|
|
||||||
@spec any?(Auth.User.t()) :: boolean()
|
@spec any?(Auth.User.t()) :: boolean()
|
||||||
def any?(user) do
|
def any?(user) do
|
||||||
user
|
user
|
||||||
|
181
lib/plausible/site/memberships/create_invitation.ex
Normal file
181
lib/plausible/site/memberships/create_invitation.ex
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
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.Billing.Quota
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
@type invite_error() ::
|
||||||
|
Ecto.Changeset.t()
|
||||||
|
| :already_a_member
|
||||||
|
| :transfer_to_self
|
||||||
|
| {:over_limit, non_neg_integer()}
|
||||||
|
| :forbidden
|
||||||
|
| :upgrade_required
|
||||||
|
|
||||||
|
@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()}
|
||||||
|
def bulk_transfer_ownership_direct(sites, new_owner) do
|
||||||
|
Plausible.Repo.transaction(fn ->
|
||||||
|
for site <- sites do
|
||||||
|
with site <- Plausible.Repo.preload(site, :owner),
|
||||||
|
:ok <- ensure_transfer_valid(site, new_owner, :owner),
|
||||||
|
{:ok, membership} <- Site.Memberships.transfer_ownership(site, new_owner) do
|
||||||
|
membership
|
||||||
|
else
|
||||||
|
{: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 <- 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 within_team_member_limit_after_transfer?(site, new_owner) do
|
||||||
|
limit = Quota.team_member_limit(new_owner)
|
||||||
|
|
||||||
|
current_usage = Quota.team_member_usage(new_owner)
|
||||||
|
site_usage = Plausible.Repo.aggregate(Quota.team_member_usage_query(site.owner, site), :count)
|
||||||
|
usage_after_transfer = current_usage + site_usage
|
||||||
|
|
||||||
|
Quota.within_limit?(usage_after_transfer, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp within_site_limit_after_transfer?(new_owner) do
|
||||||
|
limit = Quota.site_limit(new_owner)
|
||||||
|
usage_after_transfer = Quota.site_usage(new_owner) + 1
|
||||||
|
|
||||||
|
Quota.within_limit?(usage_after_transfer, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp has_access_to_site_features?(site, new_owner) do
|
||||||
|
features_to_check = [
|
||||||
|
Plausible.Billing.Feature.Props,
|
||||||
|
Plausible.Billing.Feature.RevenueGoals,
|
||||||
|
Plausible.Billing.Feature.Funnels
|
||||||
|
]
|
||||||
|
|
||||||
|
Enum.all?(features_to_check, fn feature ->
|
||||||
|
if feature.enabled?(site), do: feature.check_availability(new_owner) == :ok, else: true
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp ensure_transfer_valid(%Site{} = site, %User{} = new_owner, :owner) do
|
||||||
|
cond do
|
||||||
|
Sites.role(new_owner.id, site) == :owner -> {:error, :transfer_to_self}
|
||||||
|
not within_team_member_limit_after_transfer?(site, new_owner) -> {:error, :upgrade_required}
|
||||||
|
not within_site_limit_after_transfer?(new_owner) -> {:error, :upgrade_required}
|
||||||
|
not has_access_to_site_features?(site, new_owner) -> {:error, :upgrade_required}
|
||||||
|
true -> :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp ensure_transfer_valid(_site, _invitee, _role) do
|
||||||
|
:ok
|
||||||
|
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.within_limit?(usage, limit),
|
||||||
|
do: :ok,
|
||||||
|
else: {:error, {:over_limit, limit}}
|
||||||
|
end
|
||||||
|
end
|
@ -1,15 +1,7 @@
|
|||||||
defmodule Plausible.Sites do
|
defmodule Plausible.Sites do
|
||||||
alias Plausible.{Repo, Site, Site.SharedLink, Auth.User, Billing.Quota}
|
alias Plausible.{Repo, Site, Site.SharedLink, Billing.Quota}
|
||||||
alias PlausibleWeb.Email
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
@type invite_error() ::
|
|
||||||
Ecto.Changeset.t()
|
|
||||||
| :already_a_member
|
|
||||||
| :transfer_to_self
|
|
||||||
| {:over_limit, non_neg_integer()}
|
|
||||||
| :forbidden
|
|
||||||
|
|
||||||
def get_by_domain(domain) do
|
def get_by_domain(domain) do
|
||||||
Repo.get_by(Site, domain: domain)
|
Repo.get_by(Site, domain: domain)
|
||||||
end
|
end
|
||||||
@ -47,139 +39,6 @@ defmodule Plausible.Sites do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec bulk_transfer_ownership_direct([Site.t()], Plausible.Auth.User.t()) ::
|
|
||||||
{:ok, [Plausible.Site.Membership.t()]} | {:error, invite_error()}
|
|
||||||
def bulk_transfer_ownership_direct(sites, new_owner) do
|
|
||||||
Repo.transaction(fn ->
|
|
||||||
for site <- sites do
|
|
||||||
with :ok <- ensure_transfer_valid(site, new_owner, :owner),
|
|
||||||
{:ok, membership} <- Site.Memberships.transfer_ownership(site, new_owner) do
|
|
||||||
membership
|
|
||||||
else
|
|
||||||
{:error, error} -> Repo.rollback(error)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec bulk_transfer_ownership(
|
|
||||||
[Site.t()],
|
|
||||||
Plausible.Auth.User.t(),
|
|
||||||
String.t(),
|
|
||||||
Keyword.t()
|
|
||||||
) :: {:ok, [Plausible.Auth.Invitation.t()]} | {:error, invite_error()}
|
|
||||||
def bulk_transfer_ownership(sites, inviter, invitee_email, opts \\ []) do
|
|
||||||
Repo.transaction(fn ->
|
|
||||||
for site <- sites do
|
|
||||||
do_invite(site, inviter, invitee_email, :owner, opts)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec invite(Site.t(), Plausible.Auth.User.t(), String.t(), atom()) ::
|
|
||||||
{:ok, Plausible.Auth.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 invite(site, inviter, invitee_email, role) do
|
|
||||||
Repo.transaction(fn ->
|
|
||||||
do_invite(site, inviter, invitee_email, role)
|
|
||||||
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 :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 <- ensure_transfer_valid(site, invitee, role),
|
|
||||||
:ok <- ensure_new_membership(site, invitee, role),
|
|
||||||
%Ecto.Changeset{} = changeset <- Plausible.Auth.Invitation.new(attrs),
|
|
||||||
{:ok, invitation} <- Repo.insert(changeset) do
|
|
||||||
send_invitation_email(invitation, invitee)
|
|
||||||
invitation
|
|
||||||
else
|
|
||||||
{:error, cause} -> 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 Plausible.Site.Membership,
|
|
||||||
where: m.user_id == ^inviter.id and m.site_id == ^site.id and m.role in ^required_roles
|
|
||||||
)
|
|
||||||
|
|
||||||
if Repo.exists?(membership_query), do: :ok, else: {:error, :forbidden}
|
|
||||||
else
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp send_invitation_email(invitation, invitee) do
|
|
||||||
invitation = Repo.preload(invitation, [:site, :inviter])
|
|
||||||
|
|
||||||
email =
|
|
||||||
case {invitee, invitation.role} do
|
|
||||||
{invitee, :owner} -> Email.ownership_transfer_request(invitation, invitee)
|
|
||||||
{nil, _role} -> Email.new_user_invitation(invitation)
|
|
||||||
{%User{}, _role} -> Email.existing_user_invitation(invitation)
|
|
||||||
end
|
|
||||||
|
|
||||||
Plausible.Mailer.send(email)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_transfer_valid(site, invitee, :owner) do
|
|
||||||
if invitee && role(invitee.id, site) == :owner do
|
|
||||||
{:error, :transfer_to_self}
|
|
||||||
else
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_transfer_valid(_site, _invitee, _role) do
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_new_membership(_site, _invitee, :owner) do
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_new_membership(site, invitee, _role) do
|
|
||||||
if invitee && 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.within_limit?(usage, limit),
|
|
||||||
do: :ok,
|
|
||||||
else: {:error, {:over_limit, limit}}
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec stats_start_date(Plausible.Site.t()) :: Date.t() | nil
|
@spec stats_start_date(Plausible.Site.t()) :: Date.t() | nil
|
||||||
@doc """
|
@doc """
|
||||||
Returns the date of the first event of the given site, or `nil` if the site
|
Returns the date of the first event of the given site, or `nil` if the site
|
||||||
|
@ -13,7 +13,7 @@ defmodule PlausibleWeb.Site.MembershipController do
|
|||||||
use PlausibleWeb, :controller
|
use PlausibleWeb, :controller
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
alias Plausible.Sites
|
alias Plausible.Sites
|
||||||
alias Plausible.Site.Membership
|
alias Plausible.Site.{Membership, Memberships}
|
||||||
|
|
||||||
@only_owner_is_allowed_to [:transfer_ownership_form, :transfer_ownership]
|
@only_owner_is_allowed_to [:transfer_ownership_form, :transfer_ownership]
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ defmodule PlausibleWeb.Site.MembershipController do
|
|||||||
site_domain = conn.assigns[:site].domain
|
site_domain = conn.assigns[:site].domain
|
||||||
site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
|
site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
|
||||||
|
|
||||||
case Sites.invite(site, conn.assigns.current_user, email, role) do
|
case Memberships.create_invitation(site, conn.assigns.current_user, email, role) do
|
||||||
{:ok, invitation} ->
|
{:ok, invitation} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(
|
|> put_flash(
|
||||||
@ -98,7 +98,7 @@ defmodule PlausibleWeb.Site.MembershipController do
|
|||||||
site_domain = conn.assigns[:site].domain
|
site_domain = conn.assigns[:site].domain
|
||||||
site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
|
site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
|
||||||
|
|
||||||
case Sites.invite(site, conn.assigns.current_user, email, :owner) do
|
case Memberships.create_invitation(site, conn.assigns.current_user, email, :owner) do
|
||||||
{:ok, _invitation} ->
|
{:ok, _invitation} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:success, "Site transfer request has been sent to #{email}")
|
|> put_flash(:success, "Site transfer request has been sent to #{email}")
|
||||||
@ -111,6 +111,16 @@ defmodule PlausibleWeb.Site.MembershipController do
|
|||||||
|> put_flash(:error, "Can't transfer ownership to existing owner")
|
|> put_flash(:error, "Can't transfer ownership to existing owner")
|
||||||
|> redirect(to: Routes.site_path(conn, :settings_people, site.domain))
|
|> redirect(to: Routes.site_path(conn, :settings_people, site.domain))
|
||||||
|
|
||||||
|
{:error, :upgrade_required} ->
|
||||||
|
conn
|
||||||
|
|> put_flash(:ttl, :timer.seconds(5))
|
||||||
|
|> put_flash(:error_title, "Transfer error")
|
||||||
|
|> put_flash(
|
||||||
|
:error,
|
||||||
|
"The site you're trying to transfer exceeds the invitee's subscription plan. To proceed, please contact us at hello@plausible.io for further assistance."
|
||||||
|
)
|
||||||
|
|> redirect(to: Routes.site_path(conn, :settings_people, site.domain))
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
errors = Plausible.ChangesetHelpers.traverse_errors(changeset)
|
errors = Plausible.ChangesetHelpers.traverse_errors(changeset)
|
||||||
|
|
||||||
|
376
test/plausible/site/memberships/create_invitation_test.exs
Normal file
376
test/plausible/site/memberships/create_invitation_test.exs
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
defmodule Plausible.Site.Memberships.CreateInvitationTest do
|
||||||
|
alias Plausible.Site.Memberships.CreateInvitation
|
||||||
|
use Plausible.DataCase
|
||||||
|
use Bamboo.Test
|
||||||
|
|
||||||
|
describe "create_invitation/4" do
|
||||||
|
test "creates an invitation" do
|
||||||
|
inviter = insert(:user)
|
||||||
|
invitee = insert(:user)
|
||||||
|
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||||
|
|
||||||
|
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns validation errors" do
|
||||||
|
inviter = insert(:user)
|
||||||
|
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||||
|
|
||||||
|
assert {:error, changeset} = CreateInvitation.create_invitation(site, inviter, "", :viewer)
|
||||||
|
assert {"can't be blank", _} = changeset.errors[:email]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error when user is already a member" do
|
||||||
|
inviter = insert(:user)
|
||||||
|
invitee = insert(:user)
|
||||||
|
|
||||||
|
site =
|
||||||
|
insert(:site,
|
||||||
|
memberships: [
|
||||||
|
build(:site_membership, user: inviter, role: :owner),
|
||||||
|
build(:site_membership, user: invitee, role: :viewer)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:error, :already_a_member} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
|
||||||
|
|
||||||
|
assert {:error, :already_a_member} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, inviter.email, :viewer)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sends invitation email for existing users" do
|
||||||
|
[inviter, invitee] = insert_list(2, :user)
|
||||||
|
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||||
|
|
||||||
|
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
|
||||||
|
|
||||||
|
assert_email_delivered_with(
|
||||||
|
to: [nil: invitee.email],
|
||||||
|
subject: "[Plausible Analytics] You've been invited to #{site.domain}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sends invitation email for new users" do
|
||||||
|
inviter = insert(:user)
|
||||||
|
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||||
|
|
||||||
|
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :viewer)
|
||||||
|
|
||||||
|
assert_email_delivered_with(
|
||||||
|
to: [nil: "vini@plausible.test"],
|
||||||
|
subject: "[Plausible Analytics] You've been invited to #{site.domain}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error when owner is over their team member limit" do
|
||||||
|
[owner, inviter, invitee] = insert_list(3, :user)
|
||||||
|
|
||||||
|
memberships =
|
||||||
|
[
|
||||||
|
build(:site_membership, user: owner, role: :owner),
|
||||||
|
build(:site_membership, user: inviter, role: :admin)
|
||||||
|
] ++ build_list(4, :site_membership)
|
||||||
|
|
||||||
|
site = insert(:site, memberships: memberships)
|
||||||
|
|
||||||
|
assert {:error, {:over_limit, 5}} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sends ownership transfer email when inviter role is owner" do
|
||||||
|
inviter = insert(:user)
|
||||||
|
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||||
|
|
||||||
|
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :owner)
|
||||||
|
|
||||||
|
assert_email_delivered_with(
|
||||||
|
to: [nil: "vini@plausible.test"],
|
||||||
|
subject: "[Plausible Analytics] Request to transfer ownership of #{site.domain}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "only allows owners to transfer ownership" do
|
||||||
|
inviter = insert(:user)
|
||||||
|
|
||||||
|
site =
|
||||||
|
insert(:site,
|
||||||
|
memberships: [
|
||||||
|
build(:site_membership, user: build(:user), role: :owner),
|
||||||
|
build(:site_membership, user: inviter, role: :admin)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:error, :forbidden} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :owner)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows ownership transfer to existing site members" do
|
||||||
|
inviter = insert(:user)
|
||||||
|
invitee = insert(:user)
|
||||||
|
|
||||||
|
site =
|
||||||
|
insert(:site,
|
||||||
|
memberships: [
|
||||||
|
build(:site_membership, user: inviter, role: :owner),
|
||||||
|
build(:site_membership, user: invitee, role: :viewer)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, invitee.email, :owner)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not allow transferring ownership to existing owner" do
|
||||||
|
inviter = insert(:user, email: "vini@plausible.test")
|
||||||
|
|
||||||
|
site =
|
||||||
|
insert(:site,
|
||||||
|
memberships: [
|
||||||
|
build(:site_membership, user: inviter, role: :owner)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:error, :transfer_to_self} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :owner)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not check for limits when transferring ownership" do
|
||||||
|
inviter = insert(:user)
|
||||||
|
|
||||||
|
memberships =
|
||||||
|
[build(:site_membership, user: inviter, role: :owner)] ++ build_list(5, :site_membership)
|
||||||
|
|
||||||
|
site = insert(:site, memberships: memberships)
|
||||||
|
|
||||||
|
assert {:ok, _invitation} =
|
||||||
|
CreateInvitation.create_invitation(
|
||||||
|
site,
|
||||||
|
inviter,
|
||||||
|
"newowner@plausible.test",
|
||||||
|
:owner
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not allow viewers to invite users" do
|
||||||
|
inviter = insert(:user)
|
||||||
|
|
||||||
|
site =
|
||||||
|
insert(:site,
|
||||||
|
memberships: [
|
||||||
|
build(:site_membership, user: build(:user), role: :owner),
|
||||||
|
build(:site_membership, user: inviter, role: :viewer)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:error, :forbidden} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :viewer)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows admins to invite other admins" do
|
||||||
|
inviter = insert(:user)
|
||||||
|
|
||||||
|
site =
|
||||||
|
insert(:site,
|
||||||
|
memberships: [
|
||||||
|
build(:site_membership, user: build(:user), role: :owner),
|
||||||
|
build(:site_membership, user: inviter, role: :admin)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||||
|
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not allow transferring ownership when site does not fit the new owner subscription" do
|
||||||
|
old_owner = insert(:user, subscription: build(:business_subscription))
|
||||||
|
new_owner = insert(:user, subscription: build(:growth_subscription))
|
||||||
|
|
||||||
|
site_with_too_many_team_members =
|
||||||
|
insert(:site,
|
||||||
|
memberships:
|
||||||
|
[build(:site_membership, user: old_owner, role: :owner)] ++
|
||||||
|
build_list(6, :site_membership, role: :admin)
|
||||||
|
)
|
||||||
|
|
||||||
|
site_using_premium_features =
|
||||||
|
insert(:site,
|
||||||
|
memberships: [build(:site_membership, user: old_owner, role: :owner)],
|
||||||
|
props_enabled: true,
|
||||||
|
funnels_enabled: true
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:error, :upgrade_required} =
|
||||||
|
CreateInvitation.create_invitation(
|
||||||
|
site_with_too_many_team_members,
|
||||||
|
old_owner,
|
||||||
|
new_owner.email,
|
||||||
|
:owner
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:error, :upgrade_required} =
|
||||||
|
CreateInvitation.create_invitation(
|
||||||
|
site_using_premium_features,
|
||||||
|
old_owner,
|
||||||
|
new_owner.email,
|
||||||
|
:owner
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "bulk_create_invitation/5" do
|
||||||
|
test "initiates ownership transfer for multiple sites in one action" do
|
||||||
|
admin_user = insert(:user)
|
||||||
|
new_owner = insert(:user)
|
||||||
|
|
||||||
|
site1 =
|
||||||
|
insert(:site, memberships: [build(:site_membership, user: admin_user, role: :owner)])
|
||||||
|
|
||||||
|
site2 =
|
||||||
|
insert(:site, memberships: [build(:site_membership, user: admin_user, role: :owner)])
|
||||||
|
|
||||||
|
assert {:ok, _} =
|
||||||
|
CreateInvitation.bulk_create_invitation(
|
||||||
|
[site1, site2],
|
||||||
|
admin_user,
|
||||||
|
new_owner.email,
|
||||||
|
:owner
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_email_delivered_with(
|
||||||
|
to: [nil: new_owner.email],
|
||||||
|
subject: "[Plausible Analytics] Request to transfer ownership of #{site1.domain}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Repo.exists?(
|
||||||
|
from(i in Plausible.Auth.Invitation,
|
||||||
|
where:
|
||||||
|
i.site_id == ^site1.id and i.email == ^new_owner.email and i.role == :owner
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_invitation_exists(site1, new_owner.email, :owner)
|
||||||
|
|
||||||
|
assert_email_delivered_with(
|
||||||
|
to: [nil: new_owner.email],
|
||||||
|
subject: "[Plausible Analytics] Request to transfer ownership of #{site2.domain}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_invitation_exists(site2, new_owner.email, :owner)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "initiates ownership transfer for multiple sites in one action skipping permission checks" do
|
||||||
|
superadmin_user = insert(:user)
|
||||||
|
new_owner = insert(:user)
|
||||||
|
|
||||||
|
site1 = insert(:site)
|
||||||
|
site2 = insert(:site)
|
||||||
|
|
||||||
|
assert {:ok, _} =
|
||||||
|
CreateInvitation.bulk_create_invitation(
|
||||||
|
[site1, site2],
|
||||||
|
superadmin_user,
|
||||||
|
new_owner.email,
|
||||||
|
:owner,
|
||||||
|
check_permissions: false
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_email_delivered_with(
|
||||||
|
to: [nil: new_owner.email],
|
||||||
|
subject: "[Plausible Analytics] Request to transfer ownership of #{site1.domain}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Repo.exists?(
|
||||||
|
from(i in Plausible.Auth.Invitation,
|
||||||
|
where:
|
||||||
|
i.site_id == ^site1.id and i.email == ^new_owner.email and i.role == :owner
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_invitation_exists(site1, new_owner.email, :owner)
|
||||||
|
|
||||||
|
assert_email_delivered_with(
|
||||||
|
to: [nil: new_owner.email],
|
||||||
|
subject: "[Plausible Analytics] Request to transfer ownership of #{site2.domain}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_invitation_exists(site2, new_owner.email, :owner)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "bulk_transfer_ownership_direct/2" do
|
||||||
|
test "transfers ownership for multiple sites in one action" do
|
||||||
|
current_owner = insert(:user)
|
||||||
|
new_owner = insert(:user)
|
||||||
|
|
||||||
|
site1 =
|
||||||
|
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
||||||
|
|
||||||
|
site2 =
|
||||||
|
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
||||||
|
|
||||||
|
assert {:ok, _} = CreateInvitation.bulk_transfer_ownership_direct([site1, site2], new_owner)
|
||||||
|
|
||||||
|
assert Repo.get_by(Plausible.Site.Membership,
|
||||||
|
site_id: site1.id,
|
||||||
|
user_id: new_owner.id,
|
||||||
|
role: :owner
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Repo.get_by(Plausible.Site.Membership,
|
||||||
|
site_id: site2.id,
|
||||||
|
user_id: new_owner.id,
|
||||||
|
role: :owner
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Repo.get_by(Plausible.Site.Membership,
|
||||||
|
site_id: site1.id,
|
||||||
|
user_id: current_owner.id,
|
||||||
|
role: :admin
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Repo.get_by(Plausible.Site.Membership,
|
||||||
|
site_id: site2.id,
|
||||||
|
user_id: current_owner.id,
|
||||||
|
role: :admin
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error when user is already an owner for one of the sites" do
|
||||||
|
current_owner = insert(:user)
|
||||||
|
new_owner = insert(:user)
|
||||||
|
|
||||||
|
site1 =
|
||||||
|
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
||||||
|
|
||||||
|
site2 = insert(:site, memberships: [build(:site_membership, user: new_owner, role: :owner)])
|
||||||
|
|
||||||
|
assert {:error, :transfer_to_self} =
|
||||||
|
CreateInvitation.bulk_transfer_ownership_direct([site1, site2], new_owner)
|
||||||
|
|
||||||
|
assert Repo.get_by(Plausible.Site.Membership,
|
||||||
|
site_id: site1.id,
|
||||||
|
user_id: current_owner.id,
|
||||||
|
role: :owner
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Repo.get_by(Plausible.Site.Membership,
|
||||||
|
site_id: site2.id,
|
||||||
|
user_id: new_owner.id,
|
||||||
|
role: :owner
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp assert_invitation_exists(site, email, role) do
|
||||||
|
assert Repo.exists?(
|
||||||
|
from(i in Plausible.Auth.Invitation,
|
||||||
|
where: i.site_id == ^site.id and i.email == ^email and i.role == ^role
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
@ -1,6 +1,5 @@
|
|||||||
defmodule Plausible.SitesTest do
|
defmodule Plausible.SitesTest do
|
||||||
use Plausible.DataCase
|
use Plausible.DataCase
|
||||||
use Bamboo.Test
|
|
||||||
alias Plausible.Sites
|
alias Plausible.Sites
|
||||||
|
|
||||||
describe "is_member?" do
|
describe "is_member?" do
|
||||||
@ -68,310 +67,6 @@ defmodule Plausible.SitesTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "invite/4" do
|
|
||||||
test "creates an invitation" do
|
|
||||||
inviter = insert(:user)
|
|
||||||
invitee = insert(:user)
|
|
||||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
|
||||||
|
|
||||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
|
||||||
Sites.invite(site, inviter, invitee.email, :viewer)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns validation errors" do
|
|
||||||
inviter = insert(:user)
|
|
||||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
|
||||||
|
|
||||||
assert {:error, changeset} = Sites.invite(site, inviter, "", :viewer)
|
|
||||||
assert {"can't be blank", _} = changeset.errors[:email]
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns error when user is already a member" do
|
|
||||||
inviter = insert(:user)
|
|
||||||
invitee = insert(:user)
|
|
||||||
|
|
||||||
site =
|
|
||||||
insert(:site,
|
|
||||||
memberships: [
|
|
||||||
build(:site_membership, user: inviter, role: :owner),
|
|
||||||
build(:site_membership, user: invitee, role: :viewer)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
assert {:error, :already_a_member} = Sites.invite(site, inviter, invitee.email, :viewer)
|
|
||||||
assert {:error, :already_a_member} = Sites.invite(site, inviter, inviter.email, :viewer)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends invitation email for existing users" do
|
|
||||||
[inviter, invitee] = insert_list(2, :user)
|
|
||||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
|
||||||
|
|
||||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
|
||||||
Sites.invite(site, inviter, invitee.email, :viewer)
|
|
||||||
|
|
||||||
assert_email_delivered_with(
|
|
||||||
to: [nil: invitee.email],
|
|
||||||
subject: "[Plausible Analytics] You've been invited to #{site.domain}"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends invitation email for new users" do
|
|
||||||
inviter = insert(:user)
|
|
||||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
|
||||||
|
|
||||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
|
||||||
Sites.invite(site, inviter, "vini@plausible.test", :viewer)
|
|
||||||
|
|
||||||
assert_email_delivered_with(
|
|
||||||
to: [nil: "vini@plausible.test"],
|
|
||||||
subject: "[Plausible Analytics] You've been invited to #{site.domain}"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns error when owner is over their team member limit" do
|
|
||||||
[owner, inviter, invitee] = insert_list(3, :user)
|
|
||||||
|
|
||||||
memberships =
|
|
||||||
[
|
|
||||||
build(:site_membership, user: owner, role: :owner),
|
|
||||||
build(:site_membership, user: inviter, role: :admin)
|
|
||||||
] ++ build_list(4, :site_membership)
|
|
||||||
|
|
||||||
site = insert(:site, memberships: memberships)
|
|
||||||
assert {:error, {:over_limit, 5}} = Sites.invite(site, inviter, invitee.email, :viewer)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends ownership transfer email when inviter role is owner" do
|
|
||||||
inviter = insert(:user)
|
|
||||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
|
||||||
|
|
||||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
|
||||||
Sites.invite(site, inviter, "vini@plausible.test", :owner)
|
|
||||||
|
|
||||||
assert_email_delivered_with(
|
|
||||||
to: [nil: "vini@plausible.test"],
|
|
||||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site.domain}"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "only allows owners to transfer ownership" do
|
|
||||||
inviter = insert(:user)
|
|
||||||
|
|
||||||
site =
|
|
||||||
insert(:site,
|
|
||||||
memberships: [
|
|
||||||
build(:site_membership, user: build(:user), role: :owner),
|
|
||||||
build(:site_membership, user: inviter, role: :admin)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
assert {:error, :forbidden} = Sites.invite(site, inviter, "vini@plausible.test", :owner)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "allows ownership transfer to existing site members" do
|
|
||||||
inviter = insert(:user)
|
|
||||||
invitee = insert(:user)
|
|
||||||
|
|
||||||
site =
|
|
||||||
insert(:site,
|
|
||||||
memberships: [
|
|
||||||
build(:site_membership, user: inviter, role: :owner),
|
|
||||||
build(:site_membership, user: invitee, role: :viewer)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
|
||||||
Sites.invite(site, inviter, invitee.email, :owner)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not allow transferring ownership to existing owner" do
|
|
||||||
inviter = insert(:user, email: "vini@plausible.test")
|
|
||||||
|
|
||||||
site =
|
|
||||||
insert(:site,
|
|
||||||
memberships: [
|
|
||||||
build(:site_membership, user: inviter, role: :owner)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
assert {:error, :transfer_to_self} =
|
|
||||||
Sites.invite(site, inviter, "vini@plausible.test", :owner)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not check for limits when transferring ownership" do
|
|
||||||
inviter = insert(:user)
|
|
||||||
|
|
||||||
memberships =
|
|
||||||
[build(:site_membership, user: inviter, role: :owner)] ++ build_list(5, :site_membership)
|
|
||||||
|
|
||||||
site = insert(:site, memberships: memberships)
|
|
||||||
assert {:ok, _invitation} = Sites.invite(site, inviter, "newowner@plausible.test", :owner)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not allow viewers to invite users" do
|
|
||||||
inviter = insert(:user)
|
|
||||||
|
|
||||||
site =
|
|
||||||
insert(:site,
|
|
||||||
memberships: [
|
|
||||||
build(:site_membership, user: build(:user), role: :owner),
|
|
||||||
build(:site_membership, user: inviter, role: :viewer)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
assert {:error, :forbidden} = Sites.invite(site, inviter, "vini@plausible.test", :viewer)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "allows admins to invite other admins" do
|
|
||||||
inviter = insert(:user)
|
|
||||||
|
|
||||||
site =
|
|
||||||
insert(:site,
|
|
||||||
memberships: [
|
|
||||||
build(:site_membership, user: build(:user), role: :owner),
|
|
||||||
build(:site_membership, user: inviter, role: :admin)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
|
||||||
Sites.invite(site, inviter, "vini@plausible.test", :admin)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "bulk_transfer_ownership/4" do
|
|
||||||
test "initiates ownership transfer for multiple sites in one action" do
|
|
||||||
admin_user = insert(:user)
|
|
||||||
new_owner = insert(:user)
|
|
||||||
|
|
||||||
site1 =
|
|
||||||
insert(:site, memberships: [build(:site_membership, user: admin_user, role: :owner)])
|
|
||||||
|
|
||||||
site2 =
|
|
||||||
insert(:site, memberships: [build(:site_membership, user: admin_user, role: :owner)])
|
|
||||||
|
|
||||||
assert {:ok, _} = Sites.bulk_transfer_ownership([site1, site2], admin_user, new_owner.email)
|
|
||||||
|
|
||||||
assert_email_delivered_with(
|
|
||||||
to: [nil: new_owner.email],
|
|
||||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site1.domain}"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert Repo.exists?(
|
|
||||||
from(i in Plausible.Auth.Invitation,
|
|
||||||
where:
|
|
||||||
i.site_id == ^site1.id and i.email == ^new_owner.email and i.role == :owner
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert_invitation_exists(site1, new_owner.email, :owner)
|
|
||||||
|
|
||||||
assert_email_delivered_with(
|
|
||||||
to: [nil: new_owner.email],
|
|
||||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site2.domain}"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert_invitation_exists(site2, new_owner.email, :owner)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "initiates ownership transfer for multiple sites in one action skipping permission checks" do
|
|
||||||
superadmin_user = insert(:user)
|
|
||||||
new_owner = insert(:user)
|
|
||||||
|
|
||||||
site1 = insert(:site)
|
|
||||||
site2 = insert(:site)
|
|
||||||
|
|
||||||
assert {:ok, _} =
|
|
||||||
Sites.bulk_transfer_ownership([site1, site2], superadmin_user, new_owner.email,
|
|
||||||
check_permissions: false
|
|
||||||
)
|
|
||||||
|
|
||||||
assert_email_delivered_with(
|
|
||||||
to: [nil: new_owner.email],
|
|
||||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site1.domain}"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert Repo.exists?(
|
|
||||||
from(i in Plausible.Auth.Invitation,
|
|
||||||
where:
|
|
||||||
i.site_id == ^site1.id and i.email == ^new_owner.email and i.role == :owner
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert_invitation_exists(site1, new_owner.email, :owner)
|
|
||||||
|
|
||||||
assert_email_delivered_with(
|
|
||||||
to: [nil: new_owner.email],
|
|
||||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site2.domain}"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert_invitation_exists(site2, new_owner.email, :owner)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "bulk_transfer_ownership_direct/2" do
|
|
||||||
test "transfers ownership for multiple sites in one action" do
|
|
||||||
current_owner = insert(:user)
|
|
||||||
new_owner = insert(:user)
|
|
||||||
|
|
||||||
site1 =
|
|
||||||
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
|
||||||
|
|
||||||
site2 =
|
|
||||||
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
|
||||||
|
|
||||||
assert {:ok, _} = Sites.bulk_transfer_ownership_direct([site1, site2], new_owner)
|
|
||||||
|
|
||||||
assert Repo.get_by(Plausible.Site.Membership,
|
|
||||||
site_id: site1.id,
|
|
||||||
user_id: new_owner.id,
|
|
||||||
role: :owner
|
|
||||||
)
|
|
||||||
|
|
||||||
assert Repo.get_by(Plausible.Site.Membership,
|
|
||||||
site_id: site2.id,
|
|
||||||
user_id: new_owner.id,
|
|
||||||
role: :owner
|
|
||||||
)
|
|
||||||
|
|
||||||
assert Repo.get_by(Plausible.Site.Membership,
|
|
||||||
site_id: site1.id,
|
|
||||||
user_id: current_owner.id,
|
|
||||||
role: :admin
|
|
||||||
)
|
|
||||||
|
|
||||||
assert Repo.get_by(Plausible.Site.Membership,
|
|
||||||
site_id: site2.id,
|
|
||||||
user_id: current_owner.id,
|
|
||||||
role: :admin
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns error when user is already an owner for one of the sites" do
|
|
||||||
current_owner = insert(:user)
|
|
||||||
new_owner = insert(:user)
|
|
||||||
|
|
||||||
site1 =
|
|
||||||
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
|
||||||
|
|
||||||
site2 = insert(:site, memberships: [build(:site_membership, user: new_owner, role: :owner)])
|
|
||||||
|
|
||||||
assert {:error, :transfer_to_self} =
|
|
||||||
Sites.bulk_transfer_ownership_direct([site1, site2], new_owner)
|
|
||||||
|
|
||||||
assert Repo.get_by(Plausible.Site.Membership,
|
|
||||||
site_id: site1.id,
|
|
||||||
user_id: current_owner.id,
|
|
||||||
role: :owner
|
|
||||||
)
|
|
||||||
|
|
||||||
assert Repo.get_by(Plausible.Site.Membership,
|
|
||||||
site_id: site2.id,
|
|
||||||
user_id: new_owner.id,
|
|
||||||
role: :owner
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "get_for_user/2" do
|
describe "get_for_user/2" do
|
||||||
test "get site for super_admin" do
|
test "get site for super_admin" do
|
||||||
user1 = insert(:user)
|
user1 = insert(:user)
|
||||||
@ -386,12 +81,4 @@ defmodule Plausible.SitesTest do
|
|||||||
assert %{id: ^site_id} = Sites.get_for_user(user2.id, domain, [:super_admin])
|
assert %{id: ^site_id} = Sites.get_for_user(user2.id, domain, [:super_admin])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assert_invitation_exists(site, email, role) do
|
|
||||||
assert Repo.exists?(
|
|
||||||
from(i in Plausible.Auth.Invitation,
|
|
||||||
where: i.site_id == ^site.id and i.email == ^email and i.role == ^role
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user