forked from plausible/analytics
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement team member and invitation management actions (plausible#4977)
* Implement scaffolding for team member and invite mgmt actions * Implement updating team role * Prevent changing role if the subject is the only remaining owner * Implement removing team membership * Fix only remaining owner removal checks * Fix remove team membership service * Fix and clean up imports * Implement team invitation removal * Fix errors surfaced by dialyzer * Test and fix removing team invitations * Make accept invitation action work for team invitations * Test rejecting team invitation * Test team membership role update and removal actions * Fix flash message interpolation and missing team in transfer result * Implement migration adding UUID identifier to team * Set UUID identifier on team creation * Implement get team by identifier * Display team invitations on /sites * Test rendering team invitations on /sites * Add team management notices on /settings/people * Test showing team management notices on /settings/people * Stop drawing double horizontal rule * Add modueldoc * Handle guest member trying to call team membership endpoints gracefully --------- Co-authored-by: Adam Rutkowski <[email protected]>
- Loading branch information
Showing
21 changed files
with
863 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
defmodule Plausible.Teams.Invitations.Remove do | ||
@moduledoc """ | ||
Service for removing a team invitation. | ||
""" | ||
|
||
alias Plausible.Repo | ||
alias Plausible.Teams.Invitations | ||
alias Plausible.Teams.Memberships | ||
|
||
def remove(nil, _invitation_id, _current_user) do | ||
{:error, :permission_denied} | ||
end | ||
|
||
def remove(team, invitation_id, current_user) do | ||
with {:ok, team_invitation} <- Invitations.get_team_invitation(team, invitation_id), | ||
{:ok, current_user_role} <- Memberships.team_role(team, current_user), | ||
:ok <- check_can_remove_invitation(current_user_role) do | ||
Repo.delete!(team_invitation) | ||
|
||
{:ok, team_invitation} | ||
end | ||
end | ||
|
||
defp check_can_remove_invitation(role) do | ||
if role in [:owner, :admin] do | ||
:ok | ||
else | ||
{:error, :permission_denied} | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
defmodule Plausible.Teams.Memberships.Remove do | ||
@moduledoc """ | ||
Service for removing a team member. | ||
""" | ||
|
||
alias Plausible.Repo | ||
alias Plausible.Teams.Memberships | ||
|
||
def remove(nil, _, _), do: {:error, :permission_denied} | ||
|
||
def remove(team, user_id, current_user) do | ||
with {:ok, team_membership} <- Memberships.get_team_membership(team, user_id), | ||
{:ok, current_user_role} <- Memberships.team_role(team, current_user), | ||
:ok <- check_can_remove_membership(current_user_role, team_membership.role), | ||
:ok <- check_owner_can_get_removed(team, team_membership.role) do | ||
team_membership = Repo.preload(team_membership, [:team, :user]) | ||
Repo.delete!(team_membership) | ||
send_team_member_removed_email(team_membership) | ||
|
||
{:ok, team_membership} | ||
end | ||
end | ||
|
||
defp check_can_remove_membership(:owner, _), do: :ok | ||
defp check_can_remove_membership(:admin, role) when role != :owner, do: :ok | ||
defp check_can_remove_membership(_, _), do: {:error, :permission_denied} | ||
|
||
defp check_owner_can_get_removed(team, :owner) do | ||
if Memberships.owners_count(team) > 1 do | ||
:ok | ||
else | ||
{:error, :only_one_owner} | ||
end | ||
end | ||
|
||
defp check_owner_can_get_removed(_team, _role), do: :ok | ||
|
||
defp send_team_member_removed_email(team_membership) do | ||
team_membership | ||
|> PlausibleWeb.Email.team_member_removed() | ||
|> Plausible.Mailer.send() | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
defmodule Plausible.Teams.Memberships.UpdateRole do | ||
@moduledoc """ | ||
Service for updating role of a team member. | ||
""" | ||
|
||
alias Plausible.Repo | ||
alias Plausible.Teams | ||
alias Plausible.Teams.Memberships | ||
|
||
def update(nil, _, _, _), do: {:error, :permission_denied} | ||
|
||
def update(team, user_id, new_role_str, current_user) do | ||
new_role = String.to_existing_atom(new_role_str) | ||
|
||
with :ok <- check_valid_role(new_role), | ||
{:ok, team_membership} <- Memberships.get_team_membership(team, user_id), | ||
{:ok, current_user_role} <- Memberships.team_role(team, current_user), | ||
granting_to_self? = team_membership.user_id == user_id, | ||
:ok <- | ||
check_can_grant_role( | ||
current_user_role, | ||
team_membership.role, | ||
new_role, | ||
granting_to_self? | ||
), | ||
:ok <- check_owner_can_get_demoted(team, team_membership.role, new_role) do | ||
team_membership = | ||
team_membership | ||
|> Ecto.Changeset.change(role: new_role) | ||
|> Repo.update!() | ||
|> Repo.preload(:user) | ||
|
||
{:ok, team_membership} | ||
end | ||
end | ||
|
||
defp check_valid_role(role) do | ||
if role in (Teams.Membership.roles() -- [:guest]) do | ||
:ok | ||
else | ||
{:error, :invalid_role} | ||
end | ||
end | ||
|
||
defp check_owner_can_get_demoted(team, :owner, new_role) when new_role != :owner do | ||
if Memberships.owners_count(team) > 1 do | ||
:ok | ||
else | ||
{:error, :only_one_owner} | ||
end | ||
end | ||
|
||
defp check_owner_can_get_demoted(_team, _current_role, _new_role), do: :ok | ||
|
||
defp check_can_grant_role(user_role, _from_role, to_role, true) do | ||
if can_grant_role_to_self?(user_role, to_role) do | ||
:ok | ||
else | ||
{:error, :permission_denied} | ||
end | ||
end | ||
|
||
defp check_can_grant_role(user_role, from_role, to_role, false) do | ||
if can_grant_role_to_other?(user_role, from_role, to_role) do | ||
:ok | ||
else | ||
{:error, :permission_denied} | ||
end | ||
end | ||
|
||
defp can_grant_role_to_self?(:owner, :admin), do: true | ||
defp can_grant_role_to_self?(:owner, :editor), do: true | ||
defp can_grant_role_to_self?(:owner, :viewer), do: true | ||
defp can_grant_role_to_self?(:admin, :editor), do: true | ||
defp can_grant_role_to_self?(:admin, :viewer), do: true | ||
defp can_grant_role_to_self?(_, _), do: false | ||
|
||
defp can_grant_role_to_other?(:owner, _, _), do: true | ||
defp can_grant_role_to_other?(:admin, :admin, :admin), do: true | ||
defp can_grant_role_to_other?(:admin, :admin, :editor), do: true | ||
defp can_grant_role_to_other?(:admin, :admin, :viewer), do: true | ||
defp can_grant_role_to_other?(:admin, :editor, :admin), do: true | ||
defp can_grant_role_to_other?(:admin, :editor, :editor), do: true | ||
defp can_grant_role_to_other?(:admin, :editor, :viewer), do: true | ||
defp can_grant_role_to_other?(:admin, :viewer, :admin), do: true | ||
defp can_grant_role_to_other?(:admin, :viewer, :editor), do: true | ||
defp can_grant_role_to_other?(:admin, :viewer, :viewer), do: true | ||
defp can_grant_role_to_other?(_, _, _), do: false | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
defmodule PlausibleWeb.Team.Notice do | ||
@moduledoc """ | ||
Components with teams related notices. | ||
""" | ||
use PlausibleWeb, :component | ||
|
||
def inviting_banner(assigns) do | ||
~H""" | ||
<aside class="mt-4 mb-4"> | ||
<.notice title="Inviting people to your team" class="shadow-md dark:shadow-none mt-4"> | ||
<p> | ||
You can also invite people to your team and give them different roles like admin, editor, viewer or billing. Team members can have full access to all sites. | ||
</p> | ||
</.notice> | ||
</aside> | ||
""" | ||
end | ||
|
||
def team_members_notice(assigns) do | ||
~H""" | ||
<aside class="mt-4 mb-4"> | ||
<.notice theme={:gray} class="rounded border border-gray-300 text-sm mt-4"> | ||
<p> | ||
Team members automatically have access to this site. | ||
<.styled_link href={Routes.settings_path(PlausibleWeb.Endpoint, :team_general)}> | ||
View team members | ||
</.styled_link> | ||
</p> | ||
</.notice> | ||
</aside> | ||
""" | ||
end | ||
|
||
def team_invitations(assigns) do | ||
~H""" | ||
<aside :if={not Enum.empty?(@team_invitations)} class="mt-4 mb-4"> | ||
<.notice | ||
:for={i <- @team_invitations} | ||
id={"invitation-#{i.invitation_id}"} | ||
title="You have received team invitation" | ||
class="shadow-md dark:shadow-none mt-4" | ||
> | ||
{i.inviter.name} has invited you to join the "{i.team.name}" as {i.role} member. | ||
<.link | ||
method="post" | ||
href={Routes.invitation_path(PlausibleWeb.Endpoint, :accept_invitation, i.invitation_id)} | ||
class="whitespace-nowrap font-semibold" | ||
> | ||
Accept | ||
</.link> | ||
or | ||
<.link | ||
method="post" | ||
href={Routes.invitation_path(PlausibleWeb.Endpoint, :reject_invitation, i.invitation_id)} | ||
phx-value-invitation-id={i.invitation_id} | ||
class="whitespace-nowrap font-semibold" | ||
> | ||
Reject | ||
</.link> | ||
</.notice> | ||
</aside> | ||
""" | ||
end | ||
end |
Oops, something went wrong.