Add boolean custom field filters to member overview closes #309 #362
2 changed files with 74 additions and 28 deletions
|
|
@ -1194,32 +1194,13 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
params
|
params
|
||||||
|> Enum.filter(fn {key, _value} -> String.starts_with?(key, @boolean_filter_prefix) end)
|
|> Enum.filter(fn {key, _value} -> String.starts_with?(key, @boolean_filter_prefix) end)
|
||||||
|> Enum.reduce(%{}, fn {key, value_str}, acc ->
|
|> Enum.reduce(%{}, fn {key, value_str}, acc ->
|
||||||
# Extract custom field ID from parameter name (explicitly remove prefix)
|
process_boolean_filter_param(
|
||||||
# This is more secure than String.replace_prefix which only removes first occurrence
|
key,
|
||||||
custom_field_id_str = String.slice(key, prefix_length, String.length(key) - prefix_length)
|
value_str,
|
||||||
|
prefix_length,
|
||||||
# Validate custom field ID length (UUIDs are max @max_uuid_length characters)
|
boolean_custom_fields,
|
||||||
# This provides an additional security layer beyond UUID format validation
|
|
||||||
if String.length(custom_field_id_str) <= @max_uuid_length do
|
|
||||||
# Validate custom field ID exists and is boolean type
|
|
||||||
case Ecto.UUID.cast(custom_field_id_str) do
|
|
||||||
{:ok, _custom_field_id} ->
|
|
||||||
if Map.has_key?(boolean_custom_fields, custom_field_id_str) do
|
|
||||||
# Validate filter value
|
|
||||||
case determine_boolean_filter(value_str) do
|
|
||||||
nil -> acc
|
|
||||||
filter_value -> Map.put(acc, custom_field_id_str, filter_value)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
acc
|
|
||||||
end
|
|
||||||
|
|
||||||
:error ->
|
|
||||||
acc
|
|
||||||
end
|
|
||||||
else
|
|
||||||
acc
|
acc
|
||||||
end
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
# Security: Limit number of filters to prevent DoS attacks
|
# Security: Limit number of filters to prevent DoS attacks
|
||||||
|
|
@ -1240,6 +1221,73 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
assign(socket, :boolean_custom_field_filters, filters)
|
assign(socket, :boolean_custom_field_filters, filters)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Processes a single boolean filter parameter from URL params.
|
||||||
|
#
|
||||||
|
# Validates the parameter and adds it to the accumulator if valid.
|
||||||
|
# Returns the accumulator unchanged if validation fails.
|
||||||
|
defp process_boolean_filter_param(
|
||||||
|
key,
|
||||||
|
value_str,
|
||||||
|
prefix_length,
|
||||||
|
boolean_custom_fields,
|
||||||
|
acc
|
||||||
|
) do
|
||||||
|
# Extract custom field ID from parameter name (explicitly remove prefix)
|
||||||
|
# This is more secure than String.replace_prefix which only removes first occurrence
|
||||||
|
custom_field_id_str = String.slice(key, prefix_length, String.length(key) - prefix_length)
|
||||||
|
|
||||||
|
# Validate custom field ID length (UUIDs are max @max_uuid_length characters)
|
||||||
|
# This provides an additional security layer beyond UUID format validation
|
||||||
|
if String.length(custom_field_id_str) > @max_uuid_length do
|
||||||
|
acc
|
||||||
|
else
|
||||||
|
validate_and_add_boolean_filter(
|
||||||
|
custom_field_id_str,
|
||||||
|
value_str,
|
||||||
|
boolean_custom_fields,
|
||||||
|
acc
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validates UUID format and custom field existence, then adds filter if valid.
|
||||||
|
defp validate_and_add_boolean_filter(
|
||||||
|
custom_field_id_str,
|
||||||
|
value_str,
|
||||||
|
boolean_custom_fields,
|
||||||
|
acc
|
||||||
|
) do
|
||||||
|
case Ecto.UUID.cast(custom_field_id_str) do
|
||||||
|
{:ok, _custom_field_id} ->
|
||||||
|
add_boolean_filter_if_valid(
|
||||||
|
custom_field_id_str,
|
||||||
|
value_str,
|
||||||
|
boolean_custom_fields,
|
||||||
|
acc
|
||||||
|
)
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds boolean filter to accumulator if custom field exists and value is valid.
|
||||||
|
defp add_boolean_filter_if_valid(
|
||||||
|
custom_field_id_str,
|
||||||
|
value_str,
|
||||||
|
boolean_custom_fields,
|
||||||
|
acc
|
||||||
|
) do
|
||||||
|
if Map.has_key?(boolean_custom_fields, custom_field_id_str) do
|
||||||
|
case determine_boolean_filter(value_str) do
|
||||||
|
nil -> acc
|
||||||
|
filter_value -> Map.put(acc, custom_field_id_str, filter_value)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Determines valid boolean filter value from URL parameter.
|
# Determines valid boolean filter value from URL parameter.
|
||||||
#
|
#
|
||||||
# SECURITY: This function whitelists allowed filter values. Only "true" and "false"
|
# SECURITY: This function whitelists allowed filter values. Only "true" and "false"
|
||||||
|
|
|
||||||
|
|
@ -945,9 +945,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|
|
||||||
# Build URL with all 60 filters
|
# Build URL with all 60 filters
|
||||||
filter_params =
|
filter_params =
|
||||||
boolean_fields
|
Enum.map_join(boolean_fields, "&", fn cf -> "bf_#{cf.id}=true" end)
|
||||||
|> Enum.map(fn cf -> "bf_#{cf.id}=true" end)
|
|
||||||
|> Enum.join("&")
|
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/members?#{filter_params}")
|
{:ok, view, _html} = live(conn, "/members?#{filter_params}")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue