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("""