From 8f1a5c1a674d1f03a5062a2698f46b7fe5b26df4 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Wed, 19 Mar 2025 12:17:04 +0100 Subject: [PATCH 1/6] List only sites under team in Sites API if team id provided --- lib/plausible/sites.ex | 2 +- .../controllers/api/external_sites_controller_test.exs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/plausible/sites.ex b/lib/plausible/sites.ex index a3c801bab5aa..0d09a458e6f5 100644 --- a/lib/plausible/sites.ex +++ b/lib/plausible/sites.ex @@ -231,7 +231,7 @@ defmodule Plausible.Sites do where( query, [team_memberships: tm, guest_memberships: gm, site: s], - (tm.role != :guest and tm.team_id == ^team.id) or gm.site_id == s.id + tm.role != :guest and tm.team_id == ^team.id ) else where( diff --git a/test/plausible_web/controllers/api/external_sites_controller_test.exs b/test/plausible_web/controllers/api/external_sites_controller_test.exs index 4253930557fe..a42cc52d0699 100644 --- a/test/plausible_web/controllers/api/external_sites_controller_test.exs +++ b/test/plausible_web/controllers/api/external_sites_controller_test.exs @@ -580,8 +580,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert_matches %{ "sites" => [ - %{"domain" => ^other_team_site.domain}, - %{"domain" => ^other_site.domain} + %{"domain" => ^other_team_site.domain} ] } = json_response(conn, 200) end From 2967eebc94a9a099a5c0886cf2e0526ee36c3740 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Wed, 19 Mar 2025 12:18:12 +0100 Subject: [PATCH 2/6] Add tests for Sites API create site with team_id provided --- .../api/external_sites_controller_test.exs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/plausible_web/controllers/api/external_sites_controller_test.exs b/test/plausible_web/controllers/api/external_sites_controller_test.exs index a42cc52d0699..b8261ac9bc1a 100644 --- a/test/plausible_web/controllers/api/external_sites_controller_test.exs +++ b/test/plausible_web/controllers/api/external_sites_controller_test.exs @@ -28,6 +28,45 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do } end + test "can't create site in a team where not permitted to", %{conn: conn, user: user} do + owner = new_user() |> subscribe_to_growth_plan() + team = owner |> team_of() |> Plausible.Teams.complete_setup() + add_member(team, user: user, role: :viewer) + + conn = + post(conn, "/api/v1/sites", %{ + "team_id" => team.identifier, + "domain" => "some-site.domain", + "timezone" => "Europe/Tallinn" + }) + + assert json_response(conn, 403) == %{ + "error" => "You can't add sites to the selected team." + } + end + + test "can create a site under a specific team if permitted", %{conn: conn, user: user} do + _site = new_site(owner: user) + + owner = new_user() |> subscribe_to_growth_plan() + team = owner |> team_of() |> Plausible.Teams.complete_setup() + add_member(team, user: user, role: :owner) + + conn = + post(conn, "/api/v1/sites", %{ + "team_id" => team.identifier, + "domain" => "some-site.domain", + "timezone" => "Europe/Tallinn" + }) + + assert json_response(conn, 200) == %{ + "domain" => "some-site.domain", + "timezone" => "Europe/Tallinn" + } + + assert Repo.get_by(Plausible.Site, domain: "some-site.domain").team_id == team.id + end + test "timezone is validated", %{conn: conn} do conn = post(conn, "/api/v1/sites", %{ From 200b120a33af164efdadad6f07e9f50bea8810a0 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Wed, 19 Mar 2025 12:18:54 +0100 Subject: [PATCH 3/6] Implement `GET /api/v1/sites/teams` endpoint --- .../api/external_sites_controller.ex | 24 +++++++ lib/plausible/teams/users.ex | 36 ++++++++--- lib/plausible_web/router.ex | 1 + .../api/external_sites_controller_test.exs | 62 +++++++++++++++++++ 4 files changed, 115 insertions(+), 8 deletions(-) diff --git a/extra/lib/plausible_web/controllers/api/external_sites_controller.ex b/extra/lib/plausible_web/controllers/api/external_sites_controller.ex index 1ea08a3e08b4..586cfa557271 100644 --- a/extra/lib/plausible_web/controllers/api/external_sites_controller.ex +++ b/extra/lib/plausible_web/controllers/api/external_sites_controller.ex @@ -52,6 +52,30 @@ defmodule PlausibleWeb.Api.ExternalSitesController do end end + def teams_index(conn, params) do + user = conn.assigns.current_user + + page = + user + |> Teams.Users.teams_query(order_by: :id_desc) + |> paginate(params, @pagination_opts) + + json(conn, %{ + teams: + Enum.map(page.entries, fn team -> + api_available? = + Plausible.Billing.Feature.StatsAPI in Teams.Billing.allowed_features_for(team) + + %{ + id: team.identifier, + name: Teams.name(team), + api_available: api_available? + } + end), + meta: pagination_meta(page.metadata) + }) + end + def goals_index(conn, params) do user = conn.assigns.current_user diff --git a/lib/plausible/teams/users.ex b/lib/plausible/teams/users.ex index 60f01886f9bb..b4601e53518e 100644 --- a/lib/plausible/teams/users.ex +++ b/lib/plausible/teams/users.ex @@ -21,18 +21,38 @@ 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] - ) + user + |> teams_query(order_by: :name) |> Repo.all() |> Repo.preload(:owners) end + def teams_query(user, opts \\ []) do + order_by = Keyword.get(opts, :order_by, :name) + + query = + from( + tm in Teams.Membership, + as: :team_membership, + inner_join: t in assoc(tm, :team), + as: :team, + where: tm.user_id == ^user.id, + where: tm.role != :guest, + select: t + ) + + case order_by do + :name -> + order_by(query, [team: t], [t.name, t.id]) + + :id_desc -> + order_by(query, [team: t], desc: t.id) + + _ -> + query + end + end + def team_member?(user, opts \\ []) do excluded_team_ids = Keyword.get(opts, :except, []) diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index 10c5048cc075..ebde84bfcdc5 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -266,6 +266,7 @@ defmodule PlausibleWeb.Router do pipe_through PlausibleWeb.Plugs.AuthorizePublicAPI get "/", ExternalSitesController, :index + get "/teams", ExternalSitesController, :teams_index get "/goals", ExternalSitesController, :goals_index get "/guests", ExternalSitesController, :guests_index get "/:site_id", ExternalSitesController, :get_site diff --git a/test/plausible_web/controllers/api/external_sites_controller_test.exs b/test/plausible_web/controllers/api/external_sites_controller_test.exs index b8261ac9bc1a..5cc2d3e1f961 100644 --- a/test/plausible_web/controllers/api/external_sites_controller_test.exs +++ b/test/plausible_web/controllers/api/external_sites_controller_test.exs @@ -14,6 +14,68 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do {:ok, api_key: api_key, conn: conn} end + describe "GET /api/v1/sites/teams" do + test "shows empty list when user is not a member of any team", %{conn: conn} do + conn = get(conn, "/api/v1/sites/teams") + + assert json_response(conn, 200) == %{ + "teams" => [], + "meta" => %{ + "before" => nil, + "after" => nil, + "limit" => 100 + } + } + end + + test "shows list of teams user is a member of with api availability reflecting team state", + %{conn: conn, user: user} do + user |> subscribe_to_growth_plan() + + personal_team = team_of(user) + + owner1 = + new_user( + trial_expiry_date: Date.add(Date.utc_today(), -1), + team: [name: "Team Without Stats API"] + ) + |> subscribe_to_enterprise_plan(features: []) + + team_without_stats = owner1 |> team_of() |> Plausible.Teams.complete_setup() + add_member(team_without_stats, user: user, role: :editor) + owner2 = new_user(team: [name: "Team With Stats API"]) + team_with_stats = owner2 |> team_of() |> Plausible.Teams.complete_setup() + add_member(team_with_stats, user: user, role: :owner) + + conn = get(conn, "/api/v1/sites/teams") + + assert json_response(conn, 200) == %{ + "teams" => [ + %{ + "id" => team_with_stats.identifier, + "name" => "Team With Stats API", + "api_available" => true + }, + %{ + "id" => team_without_stats.identifier, + "name" => "Team Without Stats API", + "api_available" => false + }, + %{ + "id" => personal_team.identifier, + "name" => "My Personal Sites", + "api_available" => false + } + ], + "meta" => %{ + "before" => nil, + "after" => nil, + "limit" => 100 + } + } + end + end + describe "POST /api/v1/sites" do test "can create a site", %{conn: conn} do conn = From 5e869735c92cb200dca822c06adb37e473eae3f8 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Wed, 19 Mar 2025 12:21:25 +0100 Subject: [PATCH 4/6] Remove team identifier input from Team settings --- .../templates/settings/team_general.html.heex | 8 -------- .../controllers/settings_controller_test.exs | 1 - 2 files changed, 9 deletions(-) diff --git a/lib/plausible_web/templates/settings/team_general.html.heex b/lib/plausible_web/templates/settings/team_general.html.heex index 7315c75cf59a..e8585895a09d 100644 --- a/lib/plausible_web/templates/settings/team_general.html.heex +++ b/lib/plausible_web/templates/settings/team_general.html.heex @@ -12,14 +12,6 @@ for={@team_name_changeset} method="post" > -
- <.input_with_clipboard - name="team-identifier" - id="team-identifier" - label="Team Identifier" - value={@current_team.identifier} - /> -
<.input readonly={@current_team_role not in [:owner, :admin]} type="text" diff --git a/test/plausible_web/controllers/settings_controller_test.exs b/test/plausible_web/controllers/settings_controller_test.exs index dd51e4e839aa..6aac420b09c4 100644 --- a/test/plausible_web/controllers/settings_controller_test.exs +++ b/test/plausible_web/controllers/settings_controller_test.exs @@ -1182,7 +1182,6 @@ defmodule PlausibleWeb.SettingsControllerTest do assert html =~ "Team Information" assert html =~ "Change the name of your team" assert text_of_attr(html, "input#team_name", "value") == team.name - assert text_of_attr(html, "input#team-identifier", "value") == team.identifier end test "POST /settings/team/general/name", %{conn: conn, user: user} do From f0c5758a86869013ad29e5afb2a41b3f0cd894fd Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Wed, 19 Mar 2025 15:39:49 +0100 Subject: [PATCH 5/6] Use feature availability function --- .../plausible_web/controllers/api/external_sites_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/lib/plausible_web/controllers/api/external_sites_controller.ex b/extra/lib/plausible_web/controllers/api/external_sites_controller.ex index 586cfa557271..4e3a0bb930c2 100644 --- a/extra/lib/plausible_web/controllers/api/external_sites_controller.ex +++ b/extra/lib/plausible_web/controllers/api/external_sites_controller.ex @@ -64,7 +64,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do teams: Enum.map(page.entries, fn team -> api_available? = - Plausible.Billing.Feature.StatsAPI in Teams.Billing.allowed_features_for(team) + Plausible.Billing.Feature.StatsAPI.check_availability(team) == :ok %{ id: team.identifier, From de3fd76e89d9878c805fd020cd288aee672b3269 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 20 Mar 2025 09:24:36 +0100 Subject: [PATCH 6/6] Fix tests setup --- .../plausible_web/controllers/auth_controller_test.exs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/plausible_web/controllers/auth_controller_test.exs b/test/plausible_web/controllers/auth_controller_test.exs index 8e67e1969b00..dd4dc6d95ad2 100644 --- a/test/plausible_web/controllers/auth_controller_test.exs +++ b/test/plausible_web/controllers/auth_controller_test.exs @@ -707,7 +707,8 @@ defmodule PlausibleWeb.AuthControllerTest do } do another_owner = new_user() another_site = new_site(owner: another_owner) - add_member(another_site.team, user: user, role: :admin) + another_team = another_owner |> team_of() |> Plausible.Teams.complete_setup() + add_member(another_team, user: user, role: :admin) segment = insert(:segment, @@ -728,7 +729,8 @@ defmodule PlausibleWeb.AuthControllerTest do } do another_owner = new_user() another_site = new_site(owner: another_owner) - add_member(another_site.team, user: user, role: :admin) + another_team = another_owner |> team_of() |> Plausible.Teams.complete_setup() + add_member(another_team, user: user, role: :admin) segment = insert(:segment, @@ -749,7 +751,7 @@ defmodule PlausibleWeb.AuthControllerTest do } do another_owner = new_user() another_site = new_site(owner: another_owner) - team = team_of(another_owner) + team = another_owner |> team_of() |> Plausible.Teams.complete_setup() add_member(another_site.team, user: user, role: :owner) delete(conn, "/me") @@ -766,7 +768,7 @@ defmodule PlausibleWeb.AuthControllerTest do personal_team = team_of(user) another_owner = new_user() _another_site = new_site(owner: another_owner) - another_team = team_of(another_owner) + another_team = another_owner |> team_of() |> Plausible.Teams.complete_setup() add_member(another_team, user: user, role: :owner) delete(conn, "/me")