diff --git a/lib/plausible/data_migration/backfill_teams.ex b/lib/plausible/data_migration/backfill_teams.ex index 5ffe865a7..eb1aee2c6 100644 --- a/lib/plausible/data_migration/backfill_teams.ex +++ b/lib/plausible/data_migration/backfill_teams.ex @@ -422,8 +422,9 @@ defmodule Plausible.DataMigration.BackfillTeams do where: ti.role == :guest, where: (gi.role == :viewer and si.role == :admin) or - (gi.role == :editor and si.role == :viewer), - select: {gi, si.role} + (gi.role == :editor and si.role == :viewer) or + is_distinct(gi.invitation_id, si.invitation_id), + select: {gi, si} ) |> @repo.all(timeout: :infinity) @@ -770,7 +771,6 @@ defmodule Plausible.DataMigration.BackfillTeams do role: :guest, inviter: first_site_invitation.inviter ) - |> Ecto.Changeset.put_change(:invitation_id, first_site_invitation.invitation_id) |> Ecto.Changeset.put_change(:inserted_at, first_site_invitation.inserted_at) |> Ecto.Changeset.put_change(:updated_at, first_site_invitation.updated_at) |> @repo.insert!( @@ -784,6 +784,7 @@ defmodule Plausible.DataMigration.BackfillTeams do site_invitation.site, translate_role(site_invitation.role) ) + |> Ecto.Changeset.put_change(:invitation_id, site_invitation.invitation_id) |> Ecto.Changeset.put_change(:inserted_at, site_invitation.inserted_at) |> Ecto.Changeset.put_change(:updated_at, site_invitation.updated_at) |> @repo.insert!() @@ -795,12 +796,14 @@ defmodule Plausible.DataMigration.BackfillTeams do end) end - defp sync_guest_invitations(guest_invitations_and_roles) do - guest_invitations_and_roles + defp sync_guest_invitations(guest_and_site_invitations) do + guest_and_site_invitations |> Enum.with_index() - |> Enum.each(fn {{guest_invitation, role}, idx} -> + |> Enum.each(fn {{guest_invitation, site_invitation}, idx} -> guest_invitation - |> Ecto.Changeset.change(role: translate_role(role)) + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_change(:role, translate_role(site_invitation.role)) + |> Ecto.Changeset.put_change(:invitation_id, site_invitation.invitation_id) |> Ecto.Changeset.put_change(:updated_at, guest_invitation.updated_at) |> @repo.update!() diff --git a/lib/plausible/data_migration/teams_consistency_check.ex b/lib/plausible/data_migration/teams_consistency_check.ex index 63e59f742..e192b0405 100644 --- a/lib/plausible/data_migration/teams_consistency_check.ex +++ b/lib/plausible/data_migration/teams_consistency_check.ex @@ -290,6 +290,7 @@ defmodule Plausible.DataMigration.TeamsConsitencyCheck do where: (i.role == :viewer and parent_as(:guest_invitation).role == :viewer) or (i.role == :admin and parent_as(:guest_invitation).role == :editor), + where: i.invitation_id == parent_as(:guest_invitation).invitation_id, select: 1 ) diff --git a/lib/plausible/teams/adapter/read/sites.ex b/lib/plausible/teams/adapter/read/sites.ex index 173394c20..76ec43022 100644 --- a/lib/plausible/teams/adapter/read/sites.ex +++ b/lib/plausible/teams/adapter/read/sites.ex @@ -1,11 +1,14 @@ defmodule Plausible.Teams.Adapter.Read.Sites do @moduledoc """ - Transition adapter for new schema reads + Transition adapter for new schema reads """ + import Ecto.Query + + alias Plausible.Auth alias Plausible.Repo alias Plausible.Site - alias Plausible.Auth + alias Plausible.Teams def list(user, pagination_params, opts \\ []) do if Plausible.Teams.read_team_schemas?(user) do @@ -112,6 +115,75 @@ defmodule Plausible.Teams.Adapter.Read.Sites do %{result | entries: entries} end + def list_people(site, user) do + if Plausible.Teams.read_team_schemas?(user) do + owner_membership = + from( + tm in Teams.Membership, + where: tm.team_id == ^site.team_id, + where: tm.role == :owner, + select: %Plausible.Site.Membership{ + user_id: tm.user_id, + role: tm.role + } + ) + |> Repo.one!() + + memberships = + from( + gm in Teams.GuestMembership, + inner_join: tm in assoc(gm, :team_membership), + where: gm.site_id == ^site.id, + select: %Plausible.Site.Membership{ + user_id: tm.user_id, + role: + fragment( + """ + CASE + WHEN ? = 'editor' THEN 'admin' + ELSE ? + END + """, + gm.role, + gm.role + ) + } + ) + |> Repo.all() + + memberships = Repo.preload([owner_membership | memberships], :user) + + invitations = + from( + gi in Teams.GuestInvitation, + inner_join: ti in assoc(gi, :team_invitation), + where: gi.site_id == ^site.id, + select: %Plausible.Auth.Invitation{ + invitation_id: gi.invitation_id, + email: ti.email, + role: + fragment( + """ + CASE + WHEN ? = 'editor' THEN 'admin' + ELSE ? + END + """, + gi.role, + gi.role + ) + } + ) + |> Repo.all() + + %{memberships: memberships, invitations: invitations} + else + site + |> Repo.preload([:invitations, memberships: :user]) + |> Map.take([:memberships, :invitations]) + end + end + defp maybe_filter_by_domain(query, domain) when byte_size(domain) >= 1 and byte_size(domain) <= 64 do where(query, [s], ilike(s.domain, ^"%#{domain}%")) diff --git a/lib/plausible/teams/guest_invitation.ex b/lib/plausible/teams/guest_invitation.ex index 9f0e71002..815a608fa 100644 --- a/lib/plausible/teams/guest_invitation.ex +++ b/lib/plausible/teams/guest_invitation.ex @@ -8,6 +8,7 @@ defmodule Plausible.Teams.GuestInvitation do import Ecto.Changeset schema "guest_invitations" do + field :invitation_id, :string field :role, Ecto.Enum, values: [:viewer, :editor] belongs_to :site, Plausible.Site @@ -17,7 +18,7 @@ defmodule Plausible.Teams.GuestInvitation do end def changeset(team_invitation, site, role) do - %__MODULE__{} + %__MODULE__{invitation_id: Nanoid.generate()} |> cast(%{role: role}, [:role]) |> validate_required(:role) |> put_assoc(:team_invitation, team_invitation) diff --git a/lib/plausible/teams/invitations.ex b/lib/plausible/teams/invitations.ex index ec88523be..92ce7002b 100644 --- a/lib/plausible/teams/invitations.ex +++ b/lib/plausible/teams/invitations.ex @@ -63,7 +63,7 @@ defmodule Plausible.Teams.Invitations do site_invitation.inviter ) - guest_invitation.team_invitation + guest_invitation |> Ecto.Changeset.change(invitation_id: site_invitation.invitation_id) |> Repo.update!() end diff --git a/lib/plausible/teams/sites.ex b/lib/plausible/teams/sites.ex index 84c63757a..4d2e4995c 100644 --- a/lib/plausible/teams/sites.ex +++ b/lib/plausible/teams/sites.ex @@ -134,7 +134,8 @@ defmodule Plausible.Teams.Sites do select: %{ site_id: s.id, entry_type: "site", - invitation_id: 0, + guest_invitation_id: 0, + team_invitation_id: 0, role: tm.role, transfer_id: 0 } @@ -147,7 +148,8 @@ defmodule Plausible.Teams.Sites do select: %{ site_id: s.id, entry_type: "site", - invitation_id: 0, + guest_invitation_id: 0, + team_invitation_id: 0, role: fragment( """ @@ -184,7 +186,8 @@ defmodule Plausible.Teams.Sites do select: %{ site_id: s.id, entry_type: "invitation", - invitation_id: ti.id, + guest_invitation_id: gi.id, + team_invitation_id: ti.id, role: fragment( """ @@ -216,7 +219,8 @@ defmodule Plausible.Teams.Sites do select: %{ site_id: s.id, entry_type: "invitation", - invitation_id: 0, + guest_invitation_id: 0, + team_invitation_id: 0, role: "owner", transfer_id: st.id } @@ -234,7 +238,9 @@ defmodule Plausible.Teams.Sites do left_join: up in Site.UserPreference, on: up.site_id == s.id and up.user_id == ^user.id, left_join: ti in Teams.Invitation, - on: ti.id == u.invitation_id, + on: ti.id == u.team_invitation_id, + left_join: gi in Teams.GuestInvitation, + on: gi.id == u.guest_invitation_id, left_join: st in Teams.SiteTransfer, on: st.id == u.transfer_id, select: %{ @@ -244,10 +250,12 @@ defmodule Plausible.Teams.Sites do fragment( """ CASE + WHEN ? IS NOT NULL THEN 'invitation' WHEN ? IS NOT NULL THEN 'pinned_site' ELSE ? END """, + gi.id, up.pinned_at, u.entry_type ), @@ -263,7 +271,7 @@ defmodule Plausible.Teams.Sites do ], invitations: [ %Plausible.Auth.Invitation{ - invitation_id: coalesce(ti.invitation_id, st.transfer_id), + invitation_id: coalesce(gi.invitation_id, st.transfer_id), email: coalesce(ti.email, st.email), role: type(u.role, ^@role_type), site_id: s.id, diff --git a/lib/plausible_web/controllers/site/membership_controller.ex b/lib/plausible_web/controllers/site/membership_controller.ex index 7c69ec3db..d17849f77 100644 --- a/lib/plausible_web/controllers/site/membership_controller.ex +++ b/lib/plausible_web/controllers/site/membership_controller.ex @@ -151,10 +151,12 @@ defmodule PlausibleWeb.Site.MembershipController do |> Enum.map(fn {k, v} -> {v, k} end) |> Enum.into(%{}) - def update_role(conn, %{"id" => id, "new_role" => new_role_str}) do + def update_role_by_user(conn, %{"id" => user_id, "new_role" => new_role_str}) do %{site: site, current_user: current_user, current_user_role: current_user_role} = conn.assigns - membership = Repo.get!(Membership, id) |> Repo.preload(:user) + membership = + Membership |> Repo.get_by!(user_id: user_id, site_id: site.id) |> Repo.preload(:user) + new_role = Map.fetch!(@role_mappings, new_role_str) can_grant_role? = @@ -202,13 +204,13 @@ defmodule PlausibleWeb.Site.MembershipController do defp can_grant_role_to_other?(:admin, :viewer), do: true defp can_grant_role_to_other?(_, _), do: false - def remove_member(conn, %{"id" => id}) do - site = conn.assigns[:site] + def remove_member_by_user(conn, %{"id" => user_id} = _params) do + site = conn.assigns.site site_id = site.id membership_q = from m in Membership, - where: m.id == ^id, + where: m.user_id == ^user_id, where: m.site_id == ^site_id, inner_join: user in assoc(m, :user), inner_join: site in assoc(m, :site), diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex index 970d8983a..b2f8620f8 100644 --- a/lib/plausible_web/controllers/site_controller.ex +++ b/lib/plausible_web/controllers/site_controller.ex @@ -122,13 +122,17 @@ defmodule PlausibleWeb.SiteController do end def settings_people(conn, _params) do - site = - conn.assigns[:site] - |> Repo.preload(memberships: :user, invitations: []) + current_user = conn.assigns.current_user + site = conn.assigns.site + + %{memberships: memberships, invitations: invitations} = + Plausible.Teams.Adapter.Read.Sites.list_people(site, current_user) conn |> render("settings_people.html", site: site, + memberships: memberships, + invitations: invitations, dogfood_page_path: "/:dashboard/settings/people", layout: {PlausibleWeb.LayoutView, "site_settings.html"} ) diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index 6cedfd093..335fe37ca 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -451,8 +451,11 @@ defmodule PlausibleWeb.Router do get "/sites/:domain/transfer-ownership", Site.MembershipController, :transfer_ownership_form post "/sites/:domain/transfer-ownership", Site.MembershipController, :transfer_ownership - put "/sites/:domain/memberships/:id/role/:new_role", Site.MembershipController, :update_role - delete "/sites/:domain/memberships/:id", Site.MembershipController, :remove_member + put "/sites/:domain/memberships/u/:id/role/:new_role", + Site.MembershipController, + :update_role_by_user + + delete "/sites/:domain/memberships/u/:id", Site.MembershipController, :remove_member_by_user get "/sites/:domain/weekly-report/unsubscribe", UnsubscribeController, :weekly_report get "/sites/:domain/monthly-report/unsubscribe", UnsubscribeController, :monthly_report diff --git a/lib/plausible_web/templates/site/settings_people.html.heex b/lib/plausible_web/templates/site/settings_people.html.heex index 4fe73ef76..fdb43ddbb 100644 --- a/lib/plausible_web/templates/site/settings_people.html.heex +++ b/lib/plausible_web/templates/site/settings_people.html.heex @@ -14,8 +14,8 @@