Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
alias Plausible.Sites
alias Plausible.Goal
alias Plausible.Goals
alias Plausible.Props
alias Plausible.Teams
alias PlausibleWeb.Api.Helpers, as: H

Expand Down Expand Up @@ -445,6 +446,86 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
end
end

def custom_props_index(conn, params) do
user = conn.assigns.current_user
team = conn.assigns.current_team

with {:ok, site_id} <- expect_param_key(params, "site_id"),
{:ok, site} <- find_site(user, team, site_id, [:owner, :admin, :editor, :viewer]) do
properties =
(site.allowed_event_props || [])
|> Enum.sort()
|> Enum.map(fn prop -> %{property: prop} end)

json(conn, %{custom_properties: properties})
else
{:error, :site_not_found} ->
H.not_found(conn, "Site could not be found")

{:missing, "site_id"} ->
H.bad_request(conn, "Parameter `site_id` is required to list custom properties")
end
end

def add_custom_prop(conn, params) do
user = conn.assigns.current_user
team = conn.assigns.current_team

with {:ok, site_id} <- expect_param_key(params, "site_id"),
{:ok, property} <- expect_param_key(params, "property"),
{:ok, site} <- find_site(user, team, site_id, [:owner, :admin, :editor]),
{:ok, _} <- Props.allow(site, property) do
json(conn, %{"created" => true})
else
{:error, :site_not_found} ->
H.not_found(conn, "Site could not be found")

{:missing, param} ->
H.bad_request(conn, "Parameter `#{param}` is required to create a custom property")

{:error, changeset} ->
%{allowed_event_props: [error | _]} =
Ecto.Changeset.traverse_errors(changeset, fn {_msg, opts} ->
cond do
opts[:type] == :list and opts[:validation] == :length ->
"Can't add any more custom properties"

opts[:type] == :string and opts[:validation] == :length ->
"Parameter `property` is too long"

true ->
"Parameter `property` is invalid"
end
end)

H.bad_request(conn, error)
end
end

def delete_custom_prop(conn, params) do
user = conn.assigns.current_user
team = conn.assigns.current_team

with {:ok, site_id} <- expect_param_key(params, "site_id"),
{:ok, property} <- expect_param_key(params, "property"),
# Property name is extracted from route URL via wildcard,
# which returns a list.
property = Path.join(property),
{:ok, site} <- find_site(user, team, site_id, [:owner, :admin, :editor]),
{:ok, _} <- Props.disallow(site, property) do
json(conn, %{"deleted" => true})
else
{:error, :site_not_found} ->
H.not_found(conn, "Site could not be found")

{:missing, param} ->
H.bad_request(conn, "Parameter `#{param}` is required to delete a custom property")

e ->
H.bad_request(conn, "Something went wrong: #{inspect(e)}")
end
end

defp pagination_meta(meta) do
%{
after: meta.after,
Expand Down
6 changes: 5 additions & 1 deletion lib/plausible/props.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ defmodule Plausible.Props do
|> Ecto.Changeset.validate_change(:allowed_event_props, fn field, allowed_props ->
if Enum.all?(allowed_props, &valid?/1),
do: [],
else: [{field, "must be between 1 and #{@max_prop_key_length} characters"}]
else: [
{field,
{"must be between 1 and #{@max_prop_key_length} characters",
validation: :length, type: :string}}
]
end)
end

Expand Down
4 changes: 4 additions & 0 deletions lib/plausible_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ defmodule PlausibleWeb.Router do

scope assigns: %{api_context: :site} do
get "/goals", ExternalSitesController, :goals_index
get "/custom-props", ExternalSitesController, :custom_props_index
get "/guests", ExternalSitesController, :guests_index
get "/:site_id", ExternalSitesController, :get_site
end
Expand All @@ -353,6 +354,9 @@ defmodule PlausibleWeb.Router do
put "/goals", ExternalSitesController, :find_or_create_goal
delete "/goals/:goal_id", ExternalSitesController, :delete_goal

put "/custom-props", ExternalSitesController, :add_custom_prop
delete "/custom-props/*property", ExternalSitesController, :delete_custom_prop

put "/guests", ExternalSitesController, :find_or_create_guest
delete "/guests/:email", ExternalSitesController, :delete_guest

Expand Down
12 changes: 9 additions & 3 deletions test/plausible/props_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,23 @@ defmodule Plausible.PropsTest do

long_prop = String.duplicate("a", 301)
assert {:error, changeset} = Plausible.Props.allow(site, long_prop)
assert {"must be between 1 and 300 characters", []} == changeset.errors[:allowed_event_props]

assert {"must be between 1 and 300 characters", [validation: :length, type: :string]} ==
changeset.errors[:allowed_event_props]
end

test "allow/2 fails when prop key is empty" do
site = new_site()

assert {:error, changeset} = Plausible.Props.allow(site, "")
assert {"must be between 1 and 300 characters", []} == changeset.errors[:allowed_event_props]

assert {"must be between 1 and 300 characters", [validation: :length, type: :string]} ==
changeset.errors[:allowed_event_props]

assert {:error, changeset} = Plausible.Props.allow(site, " ")
assert {"must be between 1 and 300 characters", []} == changeset.errors[:allowed_event_props]

assert {"must be between 1 and 300 characters", [validation: :length, type: :string]} ==
changeset.errors[:allowed_event_props]
end

test "allow/2 does not fail when prop key is already in the list" do
Expand Down
Loading
Loading