From ef17f104384b28873c81eb97230f8e7f4ce392f6 Mon Sep 17 00:00:00 2001
From: Adrian Gruntkowski
Date: Thu, 6 Feb 2025 13:11:42 +0100
Subject: [PATCH] Implement basic team switcher
---
lib/plausible/teams/users.ex | 22 +++++++
.../controllers/auth_controller.ex | 66 ++++++++++++++++++-
lib/plausible_web/live/auth_context.ex | 5 ++
lib/plausible_web/plugs/auth_plug.ex | 1 +
lib/plausible_web/router.ex | 3 +
.../templates/auth/select_team.html.heex | 29 ++++++++
.../templates/layout/_header.html.heex | 12 ++++
7 files changed, 137 insertions(+), 1 deletion(-)
create mode 100644 lib/plausible_web/templates/auth/select_team.html.heex
diff --git a/lib/plausible/teams/users.ex b/lib/plausible/teams/users.ex
index 55fe12da6479..a36bbf6e151c 100644
--- a/lib/plausible/teams/users.ex
+++ b/lib/plausible/teams/users.ex
@@ -20,6 +20,28 @@ defmodule Plausible.Teams.Users do
)
end
+ def teams(user) do
+ from(
+ tm in Teams.Membership,
+ inner_join: t in assoc(tm, :team),
+ where: tm.user_id == ^user.id,
+ where: tm.role != :guest,
+ select: t,
+ order_by: [t.name, t.id]
+ )
+ |> Repo.all()
+ |> Repo.preload(:owners)
+ end
+
+ def teams_count(user) do
+ from(
+ tm in Teams.Membership,
+ where: tm.user_id == ^user.id,
+ where: tm.role != :guest
+ )
+ |> Repo.aggregate(:count)
+ end
+
def team_member?(user, opts \\ []) do
excluded_team_ids = Keyword.get(opts, :except, [])
diff --git a/lib/plausible_web/controllers/auth_controller.ex b/lib/plausible_web/controllers/auth_controller.ex
index 47e90b1f3aad..7e989ba60e27 100644
--- a/lib/plausible_web/controllers/auth_controller.ex
+++ b/lib/plausible_web/controllers/auth_controller.ex
@@ -3,6 +3,7 @@ defmodule PlausibleWeb.AuthController do
use Plausible.Repo
alias Plausible.Auth
+ alias Plausible.Teams
alias PlausibleWeb.TwoFactor
alias PlausibleWeb.UserAuth
@@ -33,7 +34,9 @@ defmodule PlausibleWeb.AuthController do
:verify_2fa_setup_form,
:verify_2fa_setup,
:disable_2fa,
- :generate_2fa_recovery_codes
+ :generate_2fa_recovery_codes,
+ :select_team,
+ :switch_team
]
)
@@ -52,6 +55,67 @@ defmodule PlausibleWeb.AuthController do
TwoFactor.Session.clear_2fa_user(conn)
end
+ def select_team(conn, _params) do
+ current_user = conn.assigns.current_user
+
+ owner_name_fn = fn owner ->
+ if owner.id == current_user.id do
+ "You"
+ else
+ owner.name
+ end
+ end
+
+ teams =
+ current_user
+ |> Teams.Users.teams()
+ |> Enum.map(fn team ->
+ current_team? = team.id == conn.assigns.current_team.id
+
+ owners =
+ Enum.map_join(team.owners, ", ", &owner_name_fn.(&1))
+
+ many_owners? = length(team.owners) > 1
+
+ %{
+ identifier: team.identifier,
+ name: team.name,
+ current?: current_team?,
+ many_owners?: many_owners?,
+ owners: owners
+ }
+ end)
+
+ render(conn, "select_team.html", teams: teams)
+ end
+
+ def switch_team(conn, params) do
+ current_user = conn.assigns.current_user
+ team = Teams.get(params["team_id"])
+
+ if team do
+ case Teams.Memberships.team_role(team, current_user) do
+ {:ok, role} when role != :guest ->
+ conn
+ |> put_session("current_team_id", team.identifier)
+ |> put_flash(
+ :success,
+ "You have switched to \"#{conn.assigns.current_team.name}\" team"
+ )
+ |> redirect(to: Routes.site_path(conn, :index))
+
+ _ ->
+ conn
+ |> put_flash(:error, "You have select an invalid team")
+ |> redirect(to: Routes.site_path(conn, :index))
+ end
+ else
+ conn
+ |> put_flash(:error, "You have select an invalid team")
+ |> redirect(to: Routes.site_path(conn, :index))
+ end
+ end
+
def activate_form(conn, params) do
user = conn.assigns.current_user
flow = params["flow"] || PlausibleWeb.Flows.register()
diff --git a/lib/plausible_web/live/auth_context.ex b/lib/plausible_web/live/auth_context.ex
index 4d2f877d44de..5c69e2248e0b 100644
--- a/lib/plausible_web/live/auth_context.ex
+++ b/lib/plausible_web/live/auth_context.ex
@@ -58,6 +58,11 @@ defmodule PlausibleWeb.Live.AuthContext do
|> assign_new(:current_team, fn context ->
context.team_from_session || context.my_team
end)
+ |> assign_new(:multiple_teams?, fn context ->
+ if context.current_user do
+ Teams.Users.teams_count(context.current_user) > 1
+ end
+ end)
{:cont, socket}
end
diff --git a/lib/plausible_web/plugs/auth_plug.ex b/lib/plausible_web/plugs/auth_plug.ex
index 24f684cdccc5..36ff83af481b 100644
--- a/lib/plausible_web/plugs/auth_plug.ex
+++ b/lib/plausible_web/plugs/auth_plug.ex
@@ -49,6 +49,7 @@ defmodule PlausibleWeb.AuthPlug do
|> assign(:current_user_session, user_session)
|> assign(:my_team, my_team)
|> assign(:current_team, current_team || my_team)
+ |> assign(:multiple_teams?, Teams.Users.teams_count(user) > 1)
_ ->
conn
diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex
index 5e8ca943bb2b..00e1356089fc 100644
--- a/lib/plausible_web/router.ex
+++ b/lib/plausible_web/router.ex
@@ -400,6 +400,9 @@ defmodule PlausibleWeb.Router do
get "/logout", AuthController, :logout
delete "/me", AuthController, :delete_me
+ get "/team/select", AuthController, :select_team
+ post "/team/select/:team_id", AuthController, :switch_team
+
get "/auth/google/callback", AuthController, :google_auth_callback
on_ee do
diff --git a/lib/plausible_web/templates/auth/select_team.html.heex b/lib/plausible_web/templates/auth/select_team.html.heex
new file mode 100644
index 000000000000..abadc7ca3f9e
--- /dev/null
+++ b/lib/plausible_web/templates/auth/select_team.html.heex
@@ -0,0 +1,29 @@
+<.focus_box>
+ <:title>Switch Team
+
+ <:subtitle>Switch your current team.
+
+
+
+ -
+ <.unstyled_link
+ method={if team.current?, do: "get", else: "post"}
+ href={
+ if team.current?,
+ do: "#",
+ else: Routes.auth_path(@conn, :switch_team, team.identifier)
+ }
+ >
+
+
+ {team.name}
+
+
+ Owner{if team.many_owners?, do: "s"}: {team.owners}
+
+
+
+
+
+
+
diff --git a/lib/plausible_web/templates/layout/_header.html.heex b/lib/plausible_web/templates/layout/_header.html.heex
index f699cb1259e7..a580fd01c219 100644
--- a/lib/plausible_web/templates/layout/_header.html.heex
+++ b/lib/plausible_web/templates/layout/_header.html.heex
@@ -72,6 +72,18 @@
{@conn.assigns[:current_user].email}
+ <.dropdown_item :if={@conn.assigns[:multiple_teams?]}>
+ Team
+
+ {@current_team.name}
+
+
+ <.dropdown_item
+ :if={@conn.assigns[:multiple_teams?]}
+ href={Routes.auth_path(@conn, :select_team)}
+ >
+ Switch Team
+
<.dropdown_divider />
<.dropdown_item href={Routes.settings_path(@conn, :index)}>
Account Settings