Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Potential improvements to identifying customers by external_id (WIP) #523

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
27 changes: 27 additions & 0 deletions lib/chat_api/customers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,27 @@ defmodule ChatApi.Customers do
Customer |> Repo.get!(id) |> Repo.preload(:tags)
end

@spec list_by_external_id(binary(), binary(), map()) :: [Customer.t()]
def list_by_external_id(external_id, account_id, filters \\ %{})

def list_by_external_id(external_id, account_id, filters) when is_binary(external_id) do
Customer
|> where(account_id: ^account_id, external_id: ^external_id)
|> where(^filter_where(filters))
|> order_by(desc: :updated_at)
|> Repo.all()
end

@spec list_by_external_id(integer(), binary(), map()) :: [Customer.t()]
def list_by_external_id(external_id, account_id, filters) when is_integer(external_id) do
external_id |> to_string() |> list_by_external_id(account_id, filters)
end

@spec find_by_external_id(binary(), binary(), map()) :: Customer.t() | nil
def find_by_external_id(external_id, account_id, filters \\ %{})

# TODO: return list rather than just one, and then ignore situations where there are multiple with the same ID
# TODO: factor in IP address as well? eh, maybe not... but it might be better to just make this super restrictive?
def find_by_external_id(external_id, account_id, filters) when is_binary(external_id) do
Customer
|> where(account_id: ^account_id, external_id: ^external_id)
Expand Down Expand Up @@ -250,6 +268,15 @@ defmodule ChatApi.Customers do
|> Repo.delete()
end

@spec is_secure_external_id?(any()) :: boolean()
def is_secure_external_id?(external_id) when is_binary(external_id) do
String.length(external_id) >= 8 &&
!String.match?(external_id, ~r/^[[:alpha:]]+$/) &&
!String.match?(external_id, ~r/^[[:digit:]]+$/)
end

def is_secure_external_id?(_external_id), do: false

# Pulled from https://hexdocs.pm/ecto/dynamic-queries.html#building-dynamic-queries
@spec filter_where(map) :: Ecto.Query.DynamicExpr.t()
def filter_where(params) do
Expand Down
50 changes: 26 additions & 24 deletions lib/chat_api_web/controllers/customer_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule ChatApiWeb.CustomerController do
alias ChatApi.{Accounts, Customers}
alias ChatApi.Customers.Customer

action_fallback ChatApiWeb.FallbackController
action_fallback(ChatApiWeb.FallbackController)

@spec index(Plug.Conn.t(), map) :: Plug.Conn.t()
def index(conn, params) do
Expand Down Expand Up @@ -48,29 +48,31 @@ defmodule ChatApiWeb.CustomerController do
"account_id" => account_id
} = params
) do
# TODO: support whitelisting urls for an account so we only enable this and
# other chat widget-related APIs for incoming requests from supported urls?
if Accounts.exists?(account_id) do
# TODO: make "host" a required param? (but would have to ignore on mobile...)
filters =
params
|> Map.take(["email", "host"])
|> Enum.reject(fn {_k, v} -> blank?(v) end)
|> Map.new()

case Customers.find_by_external_id(external_id, account_id, filters) do
%{id: customer_id} ->
json(conn, %{
data: %{
customer_id: customer_id
}
})

_ ->
json(conn, %{data: %{customer_id: nil}})
end
else
send_account_not_found_error(conn, account_id)
cond do
!Customers.is_secure_external_id?(external_id) ->
json(conn, %{data: %{customer_id: nil}})

Accounts.exists?(account_id) ->
filters =
params
|> Map.take(["email", "host"])
|> Enum.reject(fn {_k, v} -> blank?(v) end)
|> Map.new()

case Customers.list_by_external_id(external_id, account_id, filters) do
[%Customer{id: customer_id}] ->
json(conn, %{
data: %{
customer_id: customer_id
}
})

_ ->
json(conn, %{data: %{customer_id: nil}})
end

true ->
send_account_not_found_error(conn, account_id)
end
end

Expand Down