mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 17:44:43 +03:00
Add limit of 20 sites
This commit is contained in:
parent
5efa15f381
commit
119b9514b2
@ -11,6 +11,10 @@
|
||||
@apply inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md leading-5 transition hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
@apply bg-gray-400 dark:bg-gray-600
|
||||
}
|
||||
|
||||
.button-outline {
|
||||
@apply text-indigo-600 bg-transparent border border-indigo-600;
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ hcaptcha_secret = System.get_env("HCAPTCHA_SECRET")
|
||||
log_level = String.to_existing_atom(System.get_env("LOG_LEVEL", "warn"))
|
||||
log_format = System.get_env("LOG_FORMAT", "elixir")
|
||||
is_selfhost = String.to_existing_atom(System.get_env("SELFHOST", "true"))
|
||||
{site_limit, ""} = Integer.parse(System.get_env("SITE_LIMIT", "20"))
|
||||
disable_cron = String.to_existing_atom(System.get_env("DISABLE_CRON", "false"))
|
||||
|
||||
{user_agent_cache_limit, ""} = Integer.parse(System.get_env("USER_AGENT_CACHE_LIMIT", "1000"))
|
||||
@ -94,6 +95,7 @@ config :plausible,
|
||||
environment: env,
|
||||
mailer_email: mailer_email,
|
||||
admin_emails: admin_emails,
|
||||
site_limit: site_limit,
|
||||
is_selfhost: is_selfhost
|
||||
|
||||
config :plausible, :selfhost,
|
||||
|
@ -188,6 +188,20 @@ defmodule Plausible.Billing do
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of sites that an account is allowed to have. Accounts for
|
||||
grandfathering old accounts to unlimited websites and ignores site limit on self-hosted
|
||||
installations.
|
||||
"""
|
||||
@limit_accounts_after ~D[2018-05-04]
|
||||
def sites_limit(user) do
|
||||
cond do
|
||||
Timex.before?(user.inserted_at, @limit_accounts_after) -> nil
|
||||
Application.get_env(:plausible, :is_selfhost) -> nil
|
||||
true -> Application.get_env(:plausible, :site_limit)
|
||||
end
|
||||
end
|
||||
|
||||
defp format_subscription(params) do
|
||||
%{
|
||||
paddle_subscription_id: params["subscription_id"],
|
||||
|
@ -2,21 +2,28 @@ defmodule Plausible.Sites do
|
||||
use Plausible.Repo
|
||||
alias Plausible.Site.{CustomDomain, SharedLink}
|
||||
|
||||
def create(user_id, params) do
|
||||
site_changeset = Plausible.Site.changeset(%Plausible.Site{}, params)
|
||||
def create(user, params) do
|
||||
count = count_for(user)
|
||||
limit = Plausible.Billing.sites_limit(user)
|
||||
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.insert(:site, site_changeset)
|
||||
|> Ecto.Multi.run(:site_membership, fn repo, %{site: site} ->
|
||||
membership_changeset =
|
||||
Plausible.Site.Membership.changeset(%Plausible.Site.Membership{}, %{
|
||||
site_id: site.id,
|
||||
user_id: user_id
|
||||
})
|
||||
if count >= limit do
|
||||
{:error, :limit, limit}
|
||||
else
|
||||
site_changeset = Plausible.Site.changeset(%Plausible.Site{}, params)
|
||||
|
||||
repo.insert(membership_changeset)
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.insert(:site, site_changeset)
|
||||
|> Ecto.Multi.run(:site_membership, fn repo, %{site: site} ->
|
||||
membership_changeset =
|
||||
Plausible.Site.Membership.changeset(%Plausible.Site.Membership{}, %{
|
||||
site_id: site.id,
|
||||
user_id: user.id
|
||||
})
|
||||
|
||||
repo.insert(membership_changeset)
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
end
|
||||
|
||||
def create_shared_link(site, name, password \\ nil) do
|
||||
@ -51,6 +58,15 @@ defmodule Plausible.Sites do
|
||||
)
|
||||
end
|
||||
|
||||
def count_for(user) do
|
||||
Repo.aggregate(
|
||||
from(sm in Plausible.Site.Membership,
|
||||
where: sm.user_id == ^user.id
|
||||
),
|
||||
:count
|
||||
)
|
||||
end
|
||||
|
||||
def has_goals?(site) do
|
||||
Repo.exists?(
|
||||
from g in Plausible.Goal,
|
||||
|
@ -6,9 +6,9 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||
alias PlausibleWeb.Api.Helpers, as: H
|
||||
|
||||
def create_site(conn, params) do
|
||||
user_id = conn.assigns[:current_user_id]
|
||||
user = conn.assigns[:current_user]
|
||||
|
||||
case Sites.create(user_id, params) do
|
||||
case Sites.create(user, params) do
|
||||
{:ok, %{site: site}} ->
|
||||
json(conn, site)
|
||||
|
||||
@ -29,7 +29,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"),
|
||||
site when not is_nil(site) <- Sites.get_for_user(conn.assigns[:current_user_id], site_id) do
|
||||
site when not is_nil(site) <- Sites.get_for_user(conn.assigns[:current_user].id, site_id) do
|
||||
shared_link = Repo.get_by(Plausible.Site.SharedLink, site_id: site.id, name: link_name)
|
||||
|
||||
shared_link =
|
||||
|
@ -23,36 +23,31 @@ defmodule PlausibleWeb.SiteController do
|
||||
|
||||
def new(conn, _params) do
|
||||
current_user = conn.assigns[:current_user]
|
||||
changeset = Plausible.Site.changeset(%Plausible.Site{})
|
||||
site_count = Plausible.Sites.count_for(current_user)
|
||||
site_limit = Plausible.Billing.sites_limit(current_user)
|
||||
is_at_limit = site_limit && site_count >= site_limit
|
||||
is_first_site = site_count == 0
|
||||
|
||||
is_first_site =
|
||||
!Repo.exists?(
|
||||
from sm in Plausible.Site.Membership,
|
||||
where: sm.user_id == ^current_user.id
|
||||
)
|
||||
changeset = Plausible.Site.changeset(%Plausible.Site{})
|
||||
|
||||
render(conn, "new.html",
|
||||
changeset: changeset,
|
||||
is_first_site: is_first_site,
|
||||
is_at_limit: is_at_limit,
|
||||
site_limit: site_limit,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
end
|
||||
|
||||
def create_site(conn, %{"site" => site_params}) do
|
||||
user = conn.assigns[:current_user]
|
||||
site_count = Plausible.Sites.count_for(user)
|
||||
is_first_site = site_count == 0
|
||||
|
||||
case Sites.create(user.id, site_params) do
|
||||
case Sites.create(user, site_params) do
|
||||
{:ok, %{site: site}} ->
|
||||
Plausible.Slack.notify("#{user.name} created #{site.domain} [email=#{user.email}]")
|
||||
|
||||
is_first_site =
|
||||
!Repo.exists?(
|
||||
from sm in Plausible.Site.Membership,
|
||||
where:
|
||||
sm.user_id == ^user.id and
|
||||
sm.site_id != ^site.id
|
||||
)
|
||||
|
||||
if is_first_site do
|
||||
PlausibleWeb.Email.welcome_email(user)
|
||||
|> Plausible.Mailer.send_email()
|
||||
@ -63,17 +58,15 @@ defmodule PlausibleWeb.SiteController do
|
||||
|> redirect(to: "/#{URI.encode_www_form(site.domain)}/snippet")
|
||||
|
||||
{:error, :site, changeset, _} ->
|
||||
is_first_site =
|
||||
!Repo.exists?(
|
||||
from sm in Plausible.Site.Membership,
|
||||
where: sm.user_id == ^user.id
|
||||
)
|
||||
|
||||
render(conn, "new.html",
|
||||
changeset: changeset,
|
||||
is_first_site: is_first_site,
|
||||
is_at_limit: false,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
|
||||
{:error, :limit, _limit} ->
|
||||
send_resp(conn, 400, "Site limit reached")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -11,7 +11,8 @@ defmodule PlausibleWeb.AuthorizeSitesApiPlug do
|
||||
def call(conn, _opts) do
|
||||
with {:ok, raw_api_key} <- get_bearer_token(conn),
|
||||
{:ok, api_key} <- verify_access(raw_api_key) do
|
||||
assign(conn, :current_user_id, api_key.user_id)
|
||||
user = Repo.get_by(Plausible.Auth.User, id: api_key.user_id)
|
||||
assign(conn, :current_user, user)
|
||||
else
|
||||
{:error, :missing_api_key} ->
|
||||
H.unauthorized(
|
||||
|
@ -1,6 +1,27 @@
|
||||
<div class="w-full max-w-3xl mt-4 mx-auto flex">
|
||||
<%= form_for @changeset, "/sites", [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-lg rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Your website details</h2>
|
||||
<%= if @is_at_limit do %>
|
||||
<div class="rounded-md bg-yellow-50 dark:bg-transparent dark:border border-yellow-200 p-4 mt-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400 dark:text-yellow-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-400">
|
||||
Upgrade required
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
|
||||
<p>
|
||||
Your account is limited to <%= @site_limit %> sites. Please contact hello@plausible.io to add more sites.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="my-6">
|
||||
<%= label f, :domain, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-xs mt-1">Just the naked domain or subdomain without 'www'</p>
|
||||
@ -8,7 +29,7 @@
|
||||
<span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 dark:border-gray-500 bg-gray-50 dark:bg-gray-850 text-gray-500 dark:text-gray-400 sm:text-sm">
|
||||
https://
|
||||
</span>
|
||||
<%= text_input f, :domain, class: "focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 flex-1 block w-full px-3 py-2 rounded-none rounded-r-md sm:text-sm border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300", placeholder: "example.com" %>
|
||||
<%= text_input f, :domain, class: "focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 flex-1 block w-full px-3 py-2 rounded-none rounded-r-md sm:text-sm border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300", placeholder: "example.com", disabled: @is_at_limit %>
|
||||
</div>
|
||||
<%= error_tag f, :domain %>
|
||||
</div>
|
||||
@ -17,7 +38,7 @@
|
||||
<p class="text-gray-500 dark:text-gray-400 text-xs mt-1">To make sure we agree on what 'today' means</p>
|
||||
|
||||
<div class="inline-block relative w-full">
|
||||
<%= select f, :timezone, Plausible.Timezones.options(), id: "tz-select", selected: "Etc/Greenwich", class: "mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md" %>
|
||||
<%= select f, :timezone, Plausible.Timezones.options(), id: "tz-select", selected: "Etc/Greenwich", class: "mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md", disabled: @is_at_limit %>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
@ -26,7 +47,7 @@
|
||||
if (option) { option.selected = "selected"}
|
||||
</script>
|
||||
|
||||
<%= submit "Add snippet →", class: "button mt-4 w-full" %>
|
||||
<%= submit "Add snippet →", class: "button mt-4 w-full", disabled: @is_at_limit %>
|
||||
<% end %>
|
||||
|
||||
<%= if @is_first_site do %>
|
||||
|
@ -9,8 +9,23 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
|
||||
test "shows the site form", %{conn: conn} do
|
||||
conn = get(conn, "/sites/new")
|
||||
|
||||
assert html_response(conn, 200) =~ "Your website details"
|
||||
end
|
||||
|
||||
test "shows onboarding steps if it's the first site for the user", %{conn: conn} do
|
||||
conn = get(conn, "/sites/new")
|
||||
|
||||
assert html_response(conn, 200) =~ "Add site info"
|
||||
end
|
||||
|
||||
test "does not show onboarding steps if user has a site already", %{conn: conn, user: user} do
|
||||
insert(:site, members: [user], domain: "test-site.com")
|
||||
|
||||
conn = get(conn, "/sites/new")
|
||||
|
||||
refute html_response(conn, 200) =~ "Add site info"
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /sites" do
|
||||
@ -73,6 +88,26 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
assert_no_emails_delivered()
|
||||
end
|
||||
|
||||
test "does not allow site creation when the user is at their site limit", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
Application.put_env(:plausible, :site_limit, 3)
|
||||
insert(:site, members: [user])
|
||||
insert(:site, members: [user])
|
||||
insert(:site, members: [user])
|
||||
|
||||
conn =
|
||||
post(conn, "/sites", %{
|
||||
"site" => %{
|
||||
"domain" => "example.com",
|
||||
"timezone" => "Europe/London"
|
||||
}
|
||||
})
|
||||
|
||||
assert conn.status == 400
|
||||
end
|
||||
|
||||
test "cleans up the url", %{conn: conn} do
|
||||
conn =
|
||||
post(conn, "/sites", %{
|
||||
|
Loading…
Reference in New Issue
Block a user