diff --git a/config/runtime.exs b/config/runtime.exs
index e5faab8a01..47b702e5ad 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -802,6 +802,11 @@ if config_env() in [:dev, :staging, :prod, :test] do
api_key: [schema: Plausible.Auth.ApiKey, admin: Plausible.Auth.ApiKeyAdmin]
]
],
+ teams: [
+ resources: [
+ team: [schema: Plausible.Teams.Team, admin: Plausible.Teams.TeamAdmin]
+ ]
+ ],
sites: [
resources: [
site: [schema: Plausible.Site, admin: Plausible.SiteAdmin]
diff --git a/extra/lib/plausible/help_scout.ex b/extra/lib/plausible/help_scout.ex
index 11e4d9adb2..02952cc14d 100644
--- a/extra/lib/plausible/help_scout.ex
+++ b/extra/lib/plausible/help_scout.ex
@@ -90,24 +90,45 @@ defmodule Plausible.HelpScout do
plan = Billing.Plans.get_subscription_plan(team.subscription)
{team, team.subscription, plan}
+ {:error, :multiple_teams} ->
+ # NOTE: We might consider exposing the other teams later on
+ [team | _] = Plausible.Teams.Users.owned_teams(user)
+ team = Plausible.Teams.with_subscription(team)
+ plan = Billing.Plans.get_subscription_plan(team.subscription)
+ {team, team.subscription, plan}
+
{:error, :no_team} ->
{nil, nil, nil}
end
+ status_link =
+ if team do
+ Routes.kaffy_resource_url(PlausibleWeb.Endpoint, :show, :teams, :team, team.id)
+ else
+ Routes.kaffy_resource_url(PlausibleWeb.Endpoint, :show, :auth, :user, user.id)
+ end
+
+ sites_link =
+ if team do
+ Routes.kaffy_resource_url(PlausibleWeb.Endpoint, :index, :sites, :site,
+ custom_search: team.identifier
+ )
+ else
+ Routes.kaffy_resource_url(PlausibleWeb.Endpoint, :index, :sites, :site,
+ custom_search: user.email
+ )
+ end
+
{:ok,
%{
email: user.email,
notes: user.notes,
status_label: status_label(team, subscription),
- status_link:
- Routes.kaffy_resource_url(PlausibleWeb.Endpoint, :show, :auth, :user, user.id),
+ status_link: status_link,
plan_label: plan_label(subscription, plan),
plan_link: plan_link(subscription),
sites_count: Plausible.Teams.owned_sites_count(team),
- sites_link:
- Routes.kaffy_resource_url(PlausibleWeb.Endpoint, :index, :sites, :site,
- custom_search: user.email
- )
+ sites_link: sites_link
}}
end
end
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 b6d8aa1c81..0a311f49e3 100644
--- a/extra/lib/plausible_web/controllers/api/external_sites_controller.ex
+++ b/extra/lib/plausible_web/controllers/api/external_sites_controller.ex
@@ -8,16 +8,18 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
alias Plausible.Sites
alias Plausible.Goal
alias Plausible.Goals
+ alias Plausible.Teams
alias PlausibleWeb.Api.Helpers, as: H
@pagination_opts [cursor_fields: [{:id, :desc}], limit: 100, maximum_limit: 1000]
def index(conn, params) do
+ team = Teams.get(params["team_id"])
user = conn.assigns.current_user
page =
user
- |> Sites.for_user_query()
+ |> Sites.for_user_query(team)
|> paginate(params, @pagination_opts)
json(conn, %{
@@ -30,7 +32,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
user = conn.assigns.current_user
with {:ok, site_id} <- expect_param_key(params, "site_id"),
- {:ok, site} <- get_site(user, site_id, [:owner, :admin, :viewer]) do
+ {:ok, site} <- get_site(user, site_id, [:owner, :admin, :editor, :viewer]) do
page =
site
|> Plausible.Goals.for_site_query()
@@ -60,8 +62,9 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
def create_site(conn, params) do
user = conn.assigns.current_user
+ team = Plausible.Teams.get(params["team_id"])
- case Sites.create(user, params) do
+ case Sites.create(user, params, team) do
{:ok, %{site: site}} ->
json(conn, site)
@@ -73,6 +76,20 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
"Your account has reached the limit of #{limit} sites. To unlock more sites, please upgrade your subscription."
})
+ {:error, _, :permission_denied, _} ->
+ conn
+ |> put_status(403)
+ |> json(%{
+ error: "You can't add sites to the selected team."
+ })
+
+ {:error, _, :multiple_teams, _} ->
+ conn
+ |> put_status(400)
+ |> json(%{
+ error: "You must select a team with 'team_id' parameter."
+ })
+
{:error, _, changeset, _} ->
conn
|> put_status(400)
@@ -81,7 +98,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
end
def get_site(conn, %{"site_id" => site_id}) do
- case get_site(conn.assigns.current_user, site_id, [:owner, :admin, :viewer]) do
+ case get_site(conn.assigns.current_user, site_id, [:owner, :admin, :editor, :viewer]) do
{:ok, site} ->
json(conn, %{
domain: site.domain,
@@ -107,7 +124,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
def update_site(conn, %{"site_id" => site_id} = params) do
# for now this only allows to change the domain
- with {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin]),
+ with {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin, :editor]),
{:ok, site} <- Plausible.Site.Domain.change(site, params["domain"]) do
json(conn, site)
else
@@ -124,7 +141,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
def find_or_create_shared_link(conn, params) do
with {:ok, site_id} <- expect_param_key(params, "site_id"),
{:ok, link_name} <- expect_param_key(params, "name"),
- {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin]) do
+ {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin, :editor]) do
shared_link = Repo.get_by(Plausible.Site.SharedLink, site_id: site.id, name: link_name)
shared_link =
@@ -158,7 +175,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
def find_or_create_goal(conn, params) do
with {:ok, site_id} <- expect_param_key(params, "site_id"),
{:ok, _} <- expect_param_key(params, "goal_type"),
- {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin]),
+ {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin, :editor]),
{:ok, goal} <- Goals.find_or_create(site, params) do
json(conn, goal)
else
@@ -176,7 +193,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
def delete_goal(conn, params) do
with {:ok, site_id} <- expect_param_key(params, "site_id"),
{:ok, goal_id} <- expect_param_key(params, "goal_id"),
- {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin]),
+ {:ok, site} <- get_site(conn.assigns.current_user, site_id, [:owner, :admin, :editor]),
:ok <- Goals.delete(goal_id, site) do
json(conn, %{"deleted" => true})
else
diff --git a/extra/lib/plausible_web/live/funnel_settings.ex b/extra/lib/plausible_web/live/funnel_settings.ex
index f45b905fd7..ae0f852e4a 100644
--- a/extra/lib/plausible_web/live/funnel_settings.ex
+++ b/extra/lib/plausible_web/live/funnel_settings.ex
@@ -19,6 +19,7 @@ defmodule PlausibleWeb.Live.FunnelSettings do
Plausible.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
+ :editor,
:super_admin
])
end)
@@ -110,7 +111,7 @@ defmodule PlausibleWeb.Live.FunnelSettings do
Plausible.Sites.get_for_user!(
socket.assigns.current_user,
socket.assigns.domain,
- [:owner, :admin]
+ [:owner, :admin, :editor]
)
id = String.to_integer(id)
diff --git a/extra/lib/plausible_web/live/funnel_settings/form.ex b/extra/lib/plausible_web/live/funnel_settings/form.ex
index 05bcdc90ac..a9b0dabdec 100644
--- a/extra/lib/plausible_web/live/funnel_settings/form.ex
+++ b/extra/lib/plausible_web/live/funnel_settings/form.ex
@@ -16,6 +16,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, [
:owner,
:admin,
+ :editor,
:super_admin
])
diff --git a/lib/plausible/auth/auth.ex b/lib/plausible/auth/auth.ex
index 2ad4ebb295..45423cd369 100644
--- a/lib/plausible/auth/auth.ex
+++ b/lib/plausible/auth/auth.ex
@@ -7,6 +7,7 @@ defmodule Plausible.Auth do
use Plausible.Repo
alias Plausible.Auth
alias Plausible.RateLimit
+ alias Plausible.Teams
@rate_limits %{
login_ip: %{
@@ -71,9 +72,9 @@ defmodule Plausible.Auth do
def delete_user(user) do
Repo.transaction(fn ->
- case Plausible.Teams.get_by_owner(user) do
- {:ok, team} ->
- for site <- Plausible.Teams.owned_sites(team) do
+ case Teams.get_by_owner(user) do
+ {:ok, %{setup_complete: false} = team} ->
+ for site <- Teams.owned_sites(team) do
Plausible.Site.Removal.run(site)
end
@@ -84,15 +85,39 @@ defmodule Plausible.Auth do
)
Repo.delete!(team)
+ Repo.delete!(user)
- _ ->
- :skip
+ {:ok, team} ->
+ check_can_leave_team!(team)
+ Repo.delete!(user)
+
+ {:error, :multiple_teams} ->
+ check_can_leave_teams!(user)
+ Repo.delete!(user)
+
+ {:error, :no_team} ->
+ Repo.delete!(user)
end
- Repo.delete!(user)
+ :deleted
end)
end
+ defp check_can_leave_teams!(user) do
+ user
+ |> Teams.Users.owned_teams()
+ |> Enum.reject(&(&1.setup_complete == false))
+ |> Enum.map(fn team ->
+ check_can_leave_team!(team)
+ end)
+ end
+
+ defp check_can_leave_team!(team) do
+ if Teams.Memberships.owners_count(team) <= 1 do
+ Repo.rollback(:is_only_team_owner)
+ end
+ end
+
on_ee do
def is_super_admin?(nil), do: false
def is_super_admin?(%Plausible.Auth.User{id: id}), do: is_super_admin?(id)
@@ -107,17 +132,12 @@ defmodule Plausible.Auth do
@spec create_api_key(Auth.User.t(), String.t(), String.t()) ::
{:ok, Auth.ApiKey.t()} | {:error, Ecto.Changeset.t() | :upgrade_required}
def create_api_key(user, name, key) do
- team =
- case Plausible.Teams.get_by_owner(user) do
- {:ok, team} -> team
- _ -> nil
- end
-
params = %{name: name, user_id: user.id, key: key}
changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}, params)
- with :ok <- Plausible.Billing.Feature.StatsAPI.check_availability(team),
- do: Repo.insert(changeset)
+ with :ok <- check_stats_api_available(user) do
+ Repo.insert(changeset)
+ end
end
@spec delete_api_key(Auth.User.t(), integer()) :: :ok | {:error, :not_found}
@@ -148,6 +168,21 @@ defmodule Plausible.Auth do
end
end
+ defp check_stats_api_available(user) do
+ case Plausible.Teams.get_by_owner(user) do
+ {:ok, team} ->
+ Plausible.Billing.Feature.StatsAPI.check_availability(team)
+
+ {:error, :no_team} ->
+ Plausible.Billing.Feature.StatsAPI.check_availability(nil)
+
+ {:error, :multiple_teams} ->
+ # NOTE: Loophole to allow creating API keys when user is a member
+ # on multiple teams.
+ :ok
+ end
+ end
+
defp rate_limit_key(%Auth.User{id: id}), do: id
defp rate_limit_key(%Plug.Conn{} = conn), do: PlausibleWeb.RemoteIP.get(conn)
end
diff --git a/lib/plausible/auth/user.ex b/lib/plausible/auth/user.ex
index e210798f6f..ef4e0e373b 100644
--- a/lib/plausible/auth/user.ex
+++ b/lib/plausible/auth/user.ex
@@ -34,11 +34,6 @@ defmodule Plausible.Auth.User do
# Field for purely informational purposes in CRM context
field :notes, :string
- # Fields used only by CRM for mapping to the ones in the owned team
- field :trial_expiry_date, :date, virtual: true
- field :allow_next_upgrade_override, :boolean, virtual: true
- field :accept_traffic_until, :date, virtual: true
-
# Fields for TOTP authentication. See `Plausible.Auth.TOTP`.
field :totp_enabled, :boolean, default: false
field :totp_secret, Plausible.Auth.TOTP.EncryptedBinary
@@ -49,8 +44,8 @@ defmodule Plausible.Auth.User do
has_many :team_memberships, Plausible.Teams.Membership
has_many :api_keys, Plausible.Auth.ApiKey
has_one :google_auth, Plausible.Site.GoogleAuth
- has_one :owner_membership, Plausible.Teams.Membership, where: [role: :owner]
- has_one :my_team, through: [:owner_membership, :team]
+ has_many :owner_memberships, Plausible.Teams.Membership, where: [role: :owner]
+ has_many :owned_teams, through: [:owner_memberships, :team]
timestamps()
end
@@ -113,16 +108,7 @@ defmodule Plausible.Auth.User do
def changeset(user, attrs \\ %{}) do
user
- |> cast(attrs, [
- :email,
- :name,
- :email_verified,
- :theme,
- :notes,
- :trial_expiry_date,
- :allow_next_upgrade_override,
- :accept_traffic_until
- ])
+ |> cast(attrs, [:email, :name, :email_verified, :theme, :notes])
|> validate_required([:email, :name, :email_verified])
|> unique_constraint(:email)
end
diff --git a/lib/plausible/auth/user_admin.ex b/lib/plausible/auth/user_admin.ex
index 4e24f343eb..aeeab66f48 100644
--- a/lib/plausible/auth/user_admin.ex
+++ b/lib/plausible/auth/user_admin.ex
@@ -1,24 +1,13 @@
defmodule Plausible.Auth.UserAdmin do
use Plausible.Repo
use Plausible
- require Plausible.Billing.Subscription.Status
- alias Plausible.Billing.Subscription
def custom_index_query(_conn, _schema, query) do
- subscripton_q = from(s in Plausible.Billing.Subscription, order_by: [desc: s.inserted_at])
- from(r in query, preload: [my_team: [subscription: ^subscripton_q]])
+ from(r in query, preload: [:owned_teams])
end
def custom_show_query(_conn, _schema, query) do
- from(u in query,
- left_join: t in assoc(u, :my_team),
- select: %{
- u
- | trial_expiry_date: t.trial_expiry_date,
- allow_next_upgrade_override: t.allow_next_upgrade_override,
- accept_traffic_until: t.accept_traffic_until
- }
- )
+ from(u in query, preload: [:owned_teams])
end
def form_fields(_) do
@@ -26,78 +15,31 @@ defmodule Plausible.Auth.UserAdmin do
name: nil,
email: nil,
previous_email: nil,
- trial_expiry_date: %{
- help_text: "Change will also update Accept Traffic Until date"
- },
- allow_next_upgrade_override: nil,
- accept_traffic_until: %{
- help_text: "Change will take up to 15 minutes to propagate"
- },
notes: %{type: :textarea, rows: 6}
]
end
- def update(_conn, changeset) do
- my_team = Repo.preload(changeset.data, :my_team).my_team
-
- team_changed_params =
- [:trial_expiry_date, :allow_next_upgrade_override, :accept_traffic_until]
- |> Enum.map(&{&1, Ecto.Changeset.get_change(changeset, &1, :no_change)})
- |> Enum.reject(fn {_, val} -> val == :no_change end)
- |> Map.new()
-
- with {:ok, user} <- Repo.update(changeset) do
- cond do
- my_team && map_size(team_changed_params) > 0 ->
- my_team
- |> Plausible.Teams.Team.crm_sync_changeset(team_changed_params)
- |> Repo.update!()
-
- team_changed_params[:trial_expiry_date] ->
- {:ok, team} = Plausible.Teams.get_or_create(user)
-
- team
- |> Plausible.Teams.Team.crm_sync_changeset(team_changed_params)
- |> Repo.update!()
-
- true ->
- :ignore
- end
-
- {:ok, user}
- end
- end
-
def delete(_conn, %{data: user}) do
- Plausible.Auth.delete_user(user)
+ case Plausible.Auth.delete_user(user) do
+ {:ok, :deleted} ->
+ :ok
+
+ {:error, :is_only_team_owner} ->
+ "The user is the only public team owner on one or more teams."
+ end
end
def index(_) do
[
name: nil,
email: nil,
- inserted_at: %{name: "Created at", value: &format_date(&1.inserted_at)},
- trial_expiry_date: %{name: "Trial expiry", value: &format_date(&1.trial_expiry_date)},
- subscription_plan: %{value: &subscription_plan/1},
- subscription_status: %{value: &subscription_status/1},
- grace_period: %{value: &grace_period_status/1},
- accept_traffic_until: %{
- name: "Accept traffic until",
- value: &format_date(&1.accept_traffic_until)
- }
+ owned_teams: %{value: &Phoenix.HTML.raw(teams(&1.owned_teams))},
+ inserted_at: %{name: "Created at", value: &format_date(&1.inserted_at)}
]
end
def resource_actions(_) do
[
- unlock: %{
- name: "Unlock",
- action: fn _, user -> unlock(user) end
- },
- lock: %{
- name: "Lock",
- action: fn _, user -> lock(user) end
- },
reset_2fa: %{
name: "Reset 2FA",
action: fn _, user -> disable_2fa(user) end
@@ -105,94 +47,21 @@ defmodule Plausible.Auth.UserAdmin do
]
end
- defp lock(user) do
- user = Repo.preload(user, :my_team)
-
- if user.my_team && user.my_team.grace_period do
- Plausible.Billing.SiteLocker.set_lock_status_for(user.my_team, true)
- Plausible.Teams.end_grace_period(user.my_team)
- {:ok, user}
- else
- {:error, user, "No active grace period on this user"}
- end
- end
-
- defp unlock(user) do
- user = Repo.preload(user, :my_team)
-
- if user.my_team && user.my_team.grace_period do
- Plausible.Teams.remove_grace_period(user.my_team)
- Plausible.Billing.SiteLocker.set_lock_status_for(user.my_team, false)
- {:ok, user}
- else
- {:error, user, "No active grace period on this user"}
- end
- end
-
def disable_2fa(user) do
Plausible.Auth.TOTP.force_disable(user)
end
- defp grace_period_status(user) do
- grace_period = user.my_team && user.my_team.grace_period
-
- case grace_period do
- nil ->
- "--"
-
- %{manual_lock: true, is_over: true} ->
- "Manually locked"
-
- %{manual_lock: true, is_over: false} ->
- "Waiting for manual lock"
-
- %{is_over: true} ->
- "ended"
-
- %{end_date: %Date{} = end_date} ->
- days_left = Date.diff(end_date, Date.utc_today())
- "#{days_left} days left"
- end
+ def teams([]) do
+ "(none)"
end
- defp subscription_plan(user) do
- subscription = user.my_team && user.my_team.subscription
-
- if Subscription.Status.active?(subscription) && subscription.paddle_subscription_id do
- quota = PlausibleWeb.AuthView.subscription_quota(subscription)
- interval = PlausibleWeb.AuthView.subscription_interval(subscription)
-
- {:safe, ~s(#{quota} \(#{interval}\))}
- else
- "--"
- end
- end
-
- defp subscription_status(user) do
- team = user.my_team
-
- cond do
- team && team.subscription ->
- status_str =
- PlausibleWeb.SettingsView.present_subscription_status(team.subscription.status)
-
- if team.subscription.paddle_subscription_id do
- {:safe, ~s(#{status_str})}
- else
- status_str
- end
-
- Plausible.Teams.on_trial?(team) ->
- "On trial"
-
- true ->
- "Trial expired"
- end
- end
-
- defp manage_url(%{paddle_subscription_id: paddle_id} = _subscription) do
- Plausible.Billing.PaddleApi.vendors_domain() <>
- "/subscriptions/customers/manage/" <> paddle_id
+ def teams(teams) do
+ teams
+ |> Enum.map_join("
\n", fn team ->
+ """
+ #{team.name}
+ """
+ end)
end
defp format_date(nil), do: "--"
diff --git a/lib/plausible/billing/billing.ex b/lib/plausible/billing/billing.ex
index 245d78f53f..9314df1fc0 100644
--- a/lib/plausible/billing/billing.ex
+++ b/lib/plausible/billing/billing.ex
@@ -89,7 +89,7 @@ defmodule Plausible.Billing do
subscription =
Subscription
|> Repo.get_by(paddle_subscription_id: params["subscription_id"])
- |> Repo.preload(team: :owner)
+ |> Repo.preload(team: :owners)
if subscription do
changeset =
@@ -99,9 +99,11 @@ defmodule Plausible.Billing do
updated = Repo.update!(changeset)
- subscription.team.owner
- |> PlausibleWeb.Email.cancellation_email()
- |> Plausible.Mailer.send()
+ for owner <- subscription.team.owners do
+ owner
+ |> PlausibleWeb.Email.cancellation_email()
+ |> Plausible.Mailer.send()
+ end
updated
end
@@ -138,9 +140,16 @@ defmodule Plausible.Billing do
Teams.get!(team_id)
{:user_id, user_id} ->
- user = Repo.get!(Auth.User, user_id)
- {:ok, team} = Teams.get_or_create(user)
- team
+ # Given a guest or non-owner member user initiates the new subscription payment
+ # and becomes an owner of an existing team already with a subscription in between,
+ # this could result in assigning this new subscription to the newly owned team,
+ # effectively "shadowing" any old one.
+ #
+ # That's why we are always defaulting to creating a new "My Team" team regardless
+ # if they were owner of one before or not.
+ Auth.User
+ |> Repo.get!(user_id)
+ |> Teams.force_create_my_team()
end
end
@@ -212,7 +221,7 @@ defmodule Plausible.Billing do
Teams.Team
|> Repo.get!(subscription.team_id)
|> Teams.with_subscription()
- |> Repo.preload(:owner)
+ |> Repo.preload(:owners)
if subscription.id != team.subscription.id do
Sentry.capture_message("Susbscription ID mismatch",
@@ -236,7 +245,8 @@ defmodule Plausible.Billing do
)
if plan do
- api_keys = from(key in Plausible.Auth.ApiKey, where: key.user_id == ^team.owner.id)
+ owner_ids = Enum.map(team.owners, & &1.id)
+ api_keys = from(key in Plausible.Auth.ApiKey, where: key.user_id in ^owner_ids)
Repo.update_all(api_keys, set: [hourly_request_limit: plan.hourly_api_request_limit])
end
diff --git a/lib/plausible/billing/enterprise_plan.ex b/lib/plausible/billing/enterprise_plan.ex
index 36ccd0a1d6..54cd4efa11 100644
--- a/lib/plausible/billing/enterprise_plan.ex
+++ b/lib/plausible/billing/enterprise_plan.ex
@@ -24,9 +24,6 @@ defmodule Plausible.Billing.EnterprisePlan do
field :features, Plausible.Billing.Ecto.FeatureList, default: []
field :hourly_api_request_limit, :integer
- # Field used only by CRM for mapping to the ones in the owned team
- field :user_id, :integer, virtual: true
-
belongs_to :team, Plausible.Teams.Team
timestamps()
diff --git a/lib/plausible/billing/enterprise_plan_admin.ex b/lib/plausible/billing/enterprise_plan_admin.ex
index e0b56ef5a8..b8ed3fbe3d 100644
--- a/lib/plausible/billing/enterprise_plan_admin.ex
+++ b/lib/plausible/billing/enterprise_plan_admin.ex
@@ -2,7 +2,7 @@ defmodule Plausible.Billing.EnterprisePlanAdmin do
use Plausible.Repo
@numeric_fields [
- "user_id",
+ "team_id",
"paddle_plan_id",
"monthly_pageview_limit",
"site_limit",
@@ -18,7 +18,7 @@ defmodule Plausible.Billing.EnterprisePlanAdmin do
def form_fields(_schema) do
[
- user_id: nil,
+ team_id: nil,
paddle_plan_id: nil,
billing_interval: %{choices: [{"Yearly", "yearly"}, {"Monthly", "monthly"}]},
monthly_pageview_limit: nil,
@@ -40,25 +40,19 @@ defmodule Plausible.Billing.EnterprisePlanAdmin do
from(r in query,
inner_join: t in assoc(r, :team),
- inner_join: o in assoc(t, :owner),
+ inner_join: o in assoc(t, :owners),
or_where: ilike(r.paddle_plan_id, ^search_term),
- or_where: ilike(o.email, ^search_term) or ilike(o.name, ^search_term),
- preload: [team: {t, owner: o}]
- )
- end
-
- def custom_show_query(_conn, _schema, query) do
- from(ep in query,
- inner_join: t in assoc(ep, :team),
- inner_join: o in assoc(t, :owner),
- select: %{ep | user_id: o.id}
+ or_where: ilike(o.email, ^search_term),
+ or_where: ilike(o.name, ^search_term),
+ or_where: ilike(t.name, ^search_term),
+ preload: [team: {t, owners: o}]
)
end
def index(_) do
[
id: nil,
- user_email: %{value: &get_user_email/1},
+ user_email: %{value: &owner_emails(&1.team)},
paddle_plan_id: nil,
billing_interval: nil,
monthly_pageview_limit: nil,
@@ -68,20 +62,15 @@ defmodule Plausible.Billing.EnterprisePlanAdmin do
]
end
- defp get_user_email(plan), do: plan.team.owner.email
+ defp owner_emails(team) do
+ team.owners
+ |> Enum.map_join("
", & &1.email)
+ |> Phoenix.HTML.raw()
+ end
def create_changeset(schema, attrs) do
attrs = sanitize_attrs(attrs)
- team_id =
- if user_id = attrs["user_id"] do
- user = Repo.get!(Plausible.Auth.User, user_id)
- {:ok, team} = Plausible.Teams.get_or_create(user)
- team.id
- end
-
- attrs = Map.put(attrs, "team_id", team_id)
-
Plausible.Billing.EnterprisePlan.changeset(struct(schema, %{}), attrs)
end
diff --git a/lib/plausible/billing/site_locker.ex b/lib/plausible/billing/site_locker.ex
index 666bb0e048..df871331e2 100644
--- a/lib/plausible/billing/site_locker.ex
+++ b/lib/plausible/billing/site_locker.ex
@@ -26,7 +26,7 @@ defmodule Plausible.Billing.SiteLocker do
Plausible.Teams.end_grace_period(team)
if send_email? do
- team = Repo.preload(team, :owner)
+ team = Repo.preload(team, :owners)
send_grace_period_end_email(team)
end
@@ -64,8 +64,10 @@ defmodule Plausible.Billing.SiteLocker do
usage = Teams.Billing.monthly_pageview_usage(team)
suggested_plan = Plausible.Billing.Plans.suggest(team, usage.last_cycle.total)
- team.owner
- |> PlausibleWeb.Email.dashboard_locked(usage, suggested_plan)
- |> Plausible.Mailer.send()
+ for owner <- team.owners do
+ owner
+ |> PlausibleWeb.Email.dashboard_locked(usage, suggested_plan)
+ |> Plausible.Mailer.send()
+ end
end
end
diff --git a/lib/plausible/crm_extensions.ex b/lib/plausible/crm_extensions.ex
index 054b6106f0..c0aa28760e 100644
--- a/lib/plausible/crm_extensions.ex
+++ b/lib/plausible/crm_extensions.ex
@@ -9,12 +9,31 @@ defmodule Plausible.CrmExtensions do
# Kaffy uses String.to_existing_atom when listing params
@custom_search :custom_search
+ def javascripts(%{assigns: %{context: "teams", resource: "team", entry: %{} = team}}) do
+ [
+ Phoenix.HTML.raw("""
+
+ """)
+ ]
+ end
+
def javascripts(%{assigns: %{context: "auth", resource: "user", entry: %{} = user}}) do
[
Phoenix.HTML.raw("""