From 2018e7676de69e44e747299fa19f2f20ea08b4fd Mon Sep 17 00:00:00 2001 From: "construct.d" Date: Sat, 15 Nov 2025 18:49:42 +0000 Subject: [PATCH 1/2] Refactor Config and repository usage --- lib/mix/tasks/phoenix_kit.status.ex | 14 +- lib/phoenix_kit/config.ex | 150 ++++++++++++++++-- lib/phoenix_kit/emails/interceptor.ex | 2 +- lib/phoenix_kit/mailer.ex | 2 +- lib/phoenix_kit/migrations/postgres.ex | 4 +- lib/phoenix_kit/storage.ex | 3 +- lib/phoenix_kit/storage/file_server.ex | 2 +- lib/phoenix_kit/storage/url_signer.ex | 2 +- lib/phoenix_kit/users/auth/user.ex | 2 +- lib/phoenix_kit/users/rate_limiter.ex | 4 +- lib/phoenix_kit/utils/session_fingerprint.ex | 4 +- lib/phoenix_kit_web/live/users/media.ex | 6 +- .../live/users/media_detail.ex | 8 +- .../plugs/ensure_oauth_scheme.ex | 2 +- 14 files changed, 165 insertions(+), 40 deletions(-) diff --git a/lib/mix/tasks/phoenix_kit.status.ex b/lib/mix/tasks/phoenix_kit.status.ex index 9e02b369e..f3aee7abd 100644 --- a/lib/mix/tasks/phoenix_kit.status.ex +++ b/lib/mix/tasks/phoenix_kit.status.ex @@ -416,12 +416,18 @@ defmodule Mix.Tasks.PhoenixKit.Status do IO.puts("\n#{IO.ANSI.bright()}Configuration:#{IO.ANSI.reset()}") # Check layout configuration - layout_config = Application.get_env(:phoenix_kit, :layout) - IO.puts(" Layout integration: #{if layout_config, do: "Configured", else: "Using defaults"}") + layout_config = PhoenixKit.Config.get(:layout) + + IO.puts( + " Layout integration: #{if layout_config != :not_found, do: "Configured", else: "Using defaults"}" + ) # Check mailer configuration - mailer_config = Application.get_env(:phoenix_kit, PhoenixKit.Mailer) - IO.puts(" Mailer: #{if mailer_config, do: "Configured", else: "Not configured"}") + mailer_config = PhoenixKit.Config.get(PhoenixKit.Mailer) + + IO.puts( + " Mailer: #{if mailer_config != :not_found, do: "Configured", else: "Not configured"}" + ) end # Hybrid repo detection with fallback strategies diff --git a/lib/phoenix_kit/config.ex b/lib/phoenix_kit/config.ex index 15bba49c3..9095676c8 100644 --- a/lib/phoenix_kit/config.ex +++ b/lib/phoenix_kit/config.ex @@ -2,7 +2,8 @@ defmodule PhoenixKit.Config do @moduledoc """ Configuration management system for PhoenixKit. - This module provides a centralized way to manage PhoenixKit configuration. + This module provides a centralized way to manage PhoenixKit configuration + with type-safe getter functions for different data types. ## Usage @@ -13,6 +14,11 @@ defmodule PhoenixKit.Config do repo = PhoenixKit.Config.get(:repo) mailer = PhoenixKit.Config.get(:mailer, PhoenixKit.Mailer) + # Type-safe getters + options = PhoenixKit.Config.get_list(:options, []) + enabled = PhoenixKit.Config.get_boolean(:enabled, false) + host = PhoenixKit.Config.get_string(:host, "localhost") + ## Configuration Keys - `:repo` - Ecto repository module (required) @@ -22,6 +28,15 @@ defmodule PhoenixKit.Config do - `:layout_module` - Custom layout configuration - `:from_email` - Default sender email address for notifications - `:from_name` - Default sender name for notifications (default: "PhoenixKit") + + ## Type-Safe Functions + + - `get_list/2` - Gets configuration values with list type validation + - `get_boolean/2` - Gets configuration values with boolean type validation + - `get_string/2` - Gets configuration values with string type validation + + These functions provide automatic type validation and fallback to defaults + when the configuration value is missing or has the wrong type. """ @default_config [ @@ -38,7 +53,17 @@ defmodule PhoenixKit.Config do from_email: nil, from_name: "PhoenixKit", magic_link_for_login_expiry_minutes: 15, - magic_link_for_registration_expiry_minutes: 30 + magic_link_for_registration_expiry_minutes: 30, + # Security and authentication settings + password_requirements: [], + session_fingerprint_enabled: true, + session_fingerprint_strict: false, + secret_key_base: nil, + oauth_base_url: nil, + # Module-specific settings + blogging_settings_module: PhoenixKit.Settings, + # OAuth and third-party settings + ueberauth: [] ] @doc """ @@ -82,6 +107,72 @@ defmodule PhoenixKit.Config do end end + @doc """ + Gets a configuration value as a list with type validation. + + ## Examples + + iex> PhoenixKit.Config.get_list(:options, []) + [] + + iex> PhoenixKit.Config.get_list(:nonexistent, [:default]) + [:default] + + """ + @spec get_list(atom(), list()) :: list() + def get_list(key, default \\ []) + when is_atom(key) and is_list(default) do + case get(key) do + {:ok, value} when is_list(value) -> value + {:ok, _} -> default + :not_found -> default + end + end + + @doc """ + Gets a configuration value as a boolean with type validation. + + ## Examples + + iex> PhoenixKit.Config.get_boolean(:enabled, false) + true + + iex> PhoenixKit.Config.get_boolean(:nonexistent, true) + true + + """ + @spec get_boolean(atom(), boolean()) :: boolean() + def get_boolean(key, default \\ false) + when is_atom(key) and is_boolean(default) do + case get(key) do + {:ok, value} when is_boolean(value) -> value + {:ok, _} -> default + :not_found -> default + end + end + + @doc """ + Gets a configuration value as a string with type validation. + + ## Examples + + iex> PhoenixKit.Config.get_string(:host, "localhost") + "example.com" + + iex> PhoenixKit.Config.get_string(:nonexistent, "default") + "default" + + """ + @spec get_string(atom(), String.t()) :: String.t() + def get_string(key, default \\ "") + when is_atom(key) and is_binary(default) do + case get(key) do + {:ok, value} when is_binary(value) -> value + {:ok, _} -> default + :not_found -> default + end + end + @doc """ Gets the configured mailer module. @@ -130,17 +221,8 @@ defmodule PhoenixKit.Config do """ @spec get_base_url() :: String.t() def get_base_url do - host = - case get(:host) do - {:ok, host} -> host - _ -> "localhost" - end - - scheme = - case get(:scheme) do - {:ok, scheme} -> scheme - _ -> "http" - end + host = get_string(:host, "localhost") + scheme = get_string(:scheme, "http") port = case get(:port) do @@ -231,13 +313,53 @@ defmodule PhoenixKit.Config do """ @spec get_url_prefix() :: String.t() def get_url_prefix do - case get(:url_prefix, "/phoenix_kit") do + case get_string(:url_prefix, "/phoenix_kit") do nil -> "/" "" -> "/" value -> value end end + @doc """ + Gets the configured repository module. + """ + @spec get_repo() :: module() | nil + def get_repo do + case get(:repo) do + {:ok, repo} when is_atom(repo) -> repo + _ -> nil + end + end + + @doc """ + Gets the configured repository module, raising an error if not found. + + ## Examples + + iex> PhoenixKit.Config.get_repo!() + MyApp.Repo + + iex> PhoenixKit.Config.get_repo!() + ** (ArgumentError) PhoenixKit repository not configured. Please set config :phoenix_kit, repo: YourApp.Repo + + """ + @spec get_repo!() :: module() + def get_repo! do + case get(:repo) do + {:ok, repo} when is_atom(repo) -> + repo + + _ -> + raise ArgumentError, """ + PhoenixKit repository not configured. Please set: + + config :phoenix_kit, repo: YourApp.Repo + + in your application configuration. + """ + end + end + @doc """ Gets the parent application name that is using PhoenixKit. diff --git a/lib/phoenix_kit/emails/interceptor.ex b/lib/phoenix_kit/emails/interceptor.ex index a5c13cffc..7bc4e3b53 100644 --- a/lib/phoenix_kit/emails/interceptor.ex +++ b/lib/phoenix_kit/emails/interceptor.ex @@ -636,7 +636,7 @@ defmodule PhoenixKit.Emails.Interceptor do case PhoenixKit.Config.get(:mailer) do {:ok, mailer} when not is_nil(mailer) -> # Try to determine provider from mailer configuration - config = Application.get_env(:phoenix_kit, mailer, []) + config = PhoenixKit.Config.get_list(mailer, []) adapter = Keyword.get(config, :adapter) case adapter do diff --git a/lib/phoenix_kit/mailer.ex b/lib/phoenix_kit/mailer.ex index d7dcddd99..da66c1c9f 100644 --- a/lib/phoenix_kit/mailer.ex +++ b/lib/phoenix_kit/mailer.ex @@ -406,7 +406,7 @@ defmodule PhoenixKit.Mailer do # Detect provider for built-in PhoenixKit mailer defp detect_builtin_provider do - config = Application.get_env(:phoenix_kit, __MODULE__, []) + config = PhoenixKit.Config.get(PhoenixKit.Mailer, []) adapter = Keyword.get(config, :adapter) Utils.adapter_to_provider_name(adapter, "phoenix_kit_builtin") end diff --git a/lib/phoenix_kit/migrations/postgres.ex b/lib/phoenix_kit/migrations/postgres.ex index 4999eda52..6ee4ecd31 100644 --- a/lib/phoenix_kit/migrations/postgres.ex +++ b/lib/phoenix_kit/migrations/postgres.ex @@ -527,7 +527,7 @@ defmodule PhoenixKit.Migrations.Postgres do # Hybrid repo detection with fallback strategies (shared with status command) defp get_repo_with_fallback do # Strategy 1: Try to get from PhoenixKit application config - case Application.get_env(:phoenix_kit, :repo) do + case PhoenixKit.Config.get_repo() do nil -> # Strategy 2: Try to ensure PhoenixKit application is started case ensure_phoenix_kit_started() do @@ -547,7 +547,7 @@ defmodule PhoenixKit.Migrations.Postgres do # Try to start PhoenixKit application and get repo config defp ensure_phoenix_kit_started do Application.ensure_all_started(:phoenix_kit) - Application.get_env(:phoenix_kit, :repo) + PhoenixKit.Config.get_repo() rescue _ -> nil end diff --git a/lib/phoenix_kit/storage.ex b/lib/phoenix_kit/storage.ex index 4e9360031..34183e5a5 100644 --- a/lib/phoenix_kit/storage.ex +++ b/lib/phoenix_kit/storage.ex @@ -1237,8 +1237,7 @@ defmodule PhoenixKit.Storage do # ===== REPO HELPERS ===== defp repo do - # Get the repository from application config or use a default - Application.get_env(:phoenix_kit, :repo) || PhoenixKit.Repo + PhoenixKit.Config.get_repo() end # Query builders for file listing diff --git a/lib/phoenix_kit/storage/file_server.ex b/lib/phoenix_kit/storage/file_server.ex index 350676e33..e485cae50 100644 --- a/lib/phoenix_kit/storage/file_server.ex +++ b/lib/phoenix_kit/storage/file_server.ex @@ -266,6 +266,6 @@ defmodule PhoenixKit.Storage.FileServer do @doc false defp get_repo do - Application.get_env(:phoenix_kit, :repo) || raise "PhoenixKit repo not configured" + PhoenixKit.Config.get_repo!() end end diff --git a/lib/phoenix_kit/storage/url_signer.ex b/lib/phoenix_kit/storage/url_signer.ex index ca2577398..2ef4b9d88 100644 --- a/lib/phoenix_kit/storage/url_signer.ex +++ b/lib/phoenix_kit/storage/url_signer.ex @@ -135,7 +135,7 @@ defmodule PhoenixKit.Storage.URLSigner do # 1. Explicitly configured on :phoenix_kit # 2. From the configured endpoint # 3. Return nil if not found (will use data without secret) - Application.get_env(:phoenix_kit, :secret_key_base) || + PhoenixKit.Config.get(:secret_key_base, nil) || get_endpoint_secret() end diff --git a/lib/phoenix_kit/users/auth/user.ex b/lib/phoenix_kit/users/auth/user.ex index a90998704..421f35462 100644 --- a/lib/phoenix_kit/users/auth/user.ex +++ b/lib/phoenix_kit/users/auth/user.ex @@ -152,7 +152,7 @@ defmodule PhoenixKit.Users.Auth.User do # - require_digit: false # - require_special: false defp apply_password_requirements(changeset) do - requirements = Application.get_env(:phoenix_kit, :password_requirements, []) + requirements = PhoenixKit.Config.get_list(:password_requirements, []) changeset |> validate_length(:password, diff --git a/lib/phoenix_kit/users/rate_limiter.ex b/lib/phoenix_kit/users/rate_limiter.ex index 1726cfd16..e94489a17 100644 --- a/lib/phoenix_kit/users/rate_limiter.ex +++ b/lib/phoenix_kit/users/rate_limiter.ex @@ -370,8 +370,8 @@ defmodule PhoenixKit.Users.RateLimiter do |> String.downcase() end - defp get_config do - Application.get_env(:phoenix_kit, __MODULE__, []) + def get_config do + PhoenixKit.Config.get(__MODULE__, []) |> Keyword.merge(@default_config, fn _k, v1, _v2 -> v1 end) end diff --git a/lib/phoenix_kit/utils/session_fingerprint.ex b/lib/phoenix_kit/utils/session_fingerprint.ex index a294bbcb4..10b7c395f 100644 --- a/lib/phoenix_kit/utils/session_fingerprint.ex +++ b/lib/phoenix_kit/utils/session_fingerprint.ex @@ -190,7 +190,7 @@ defmodule PhoenixKit.Utils.SessionFingerprint do """ def fingerprinting_enabled? do - Application.get_env(:phoenix_kit, :session_fingerprint_enabled, true) + PhoenixKit.Config.get_boolean(:session_fingerprint_enabled, true) end @doc """ @@ -206,7 +206,7 @@ defmodule PhoenixKit.Utils.SessionFingerprint do """ def strict_mode? do - Application.get_env(:phoenix_kit, :session_fingerprint_strict, false) + PhoenixKit.Config.get_boolean(:session_fingerprint_strict, false) end # Private helper to get a header value from connection diff --git a/lib/phoenix_kit_web/live/users/media.ex b/lib/phoenix_kit_web/live/users/media.ex index 5ce38d003..54a63a093 100644 --- a/lib/phoenix_kit_web/live/users/media.ex +++ b/lib/phoenix_kit_web/live/users/media.ex @@ -9,6 +9,8 @@ defmodule PhoenixKitWeb.Live.Users.Media do require Logger + import Ecto.Query + alias PhoenixKit.Settings alias PhoenixKit.Storage.FileInstance alias PhoenixKit.Storage.URLSigner @@ -183,9 +185,7 @@ defmodule PhoenixKitWeb.Live.Users.Media do # Load existing files from database with pagination defp load_existing_files(page, per_page) do - import Ecto.Query - - repo = Application.get_env(:phoenix_kit, :repo) + repo = PhoenixKit.Config.get_repo() # Get total count total_count = repo.aggregate(PhoenixKit.Storage.File, :count, :id) diff --git a/lib/phoenix_kit_web/live/users/media_detail.ex b/lib/phoenix_kit_web/live/users/media_detail.ex index c96a93f4b..9c453b3c6 100644 --- a/lib/phoenix_kit_web/live/users/media_detail.ex +++ b/lib/phoenix_kit_web/live/users/media_detail.ex @@ -9,6 +9,8 @@ defmodule PhoenixKitWeb.Live.Users.MediaDetail do require Logger + import Ecto.Query + alias PhoenixKit.Settings alias PhoenixKit.Storage alias PhoenixKit.Storage.File @@ -96,7 +98,7 @@ defmodule PhoenixKitWeb.Live.Users.MediaDetail do end defp load_file_data(socket, file_id) do - repo = Application.get_env(:phoenix_kit, :repo) + repo = PhoenixKit.Config.get_repo() case repo.get(File, file_id) do nil -> @@ -122,8 +124,6 @@ defmodule PhoenixKitWeb.Live.Users.MediaDetail do end defp load_file_instances(file_id, repo) do - import Ecto.Query - FileInstance |> where([fi], fi.file_id == ^file_id) |> repo.all() @@ -186,8 +186,6 @@ defmodule PhoenixKitWeb.Live.Users.MediaDetail do # Load file locations with bucket information defp load_file_locations(file_instance_id, repo) do - import Ecto.Query - FileLocation |> where([fl], fl.file_instance_id == ^file_instance_id and fl.status == "active") |> preload(:bucket) diff --git a/lib/phoenix_kit_web/plugs/ensure_oauth_scheme.ex b/lib/phoenix_kit_web/plugs/ensure_oauth_scheme.ex index 36ffa3d74..5980757f5 100644 --- a/lib/phoenix_kit_web/plugs/ensure_oauth_scheme.ex +++ b/lib/phoenix_kit_web/plugs/ensure_oauth_scheme.ex @@ -31,7 +31,7 @@ defmodule PhoenixKitWeb.Plugs.EnsureOAuthScheme do apply_scheme(conn, forwarded_proto) # 2. Check explicit oauth_base_url config - base_url = Application.get_env(:phoenix_kit, :oauth_base_url) -> + base_url = PhoenixKit.Config.get_string(:oauth_base_url) -> apply_base_url(conn, base_url) # 3. Check endpoint URL config From 0482a4174a69f67bfea8dde47ee7986dc5e3e4f5 Mon Sep 17 00:00:00 2001 From: "construct.d" Date: Sat, 15 Nov 2025 22:12:18 +0000 Subject: [PATCH 2/2] Fix pattern matching error --- lib/phoenix_kit/config.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/phoenix_kit/config.ex b/lib/phoenix_kit/config.ex index 9095676c8..32cccb378 100644 --- a/lib/phoenix_kit/config.ex +++ b/lib/phoenix_kit/config.ex @@ -314,7 +314,6 @@ defmodule PhoenixKit.Config do @spec get_url_prefix() :: String.t() def get_url_prefix do case get_string(:url_prefix, "/phoenix_kit") do - nil -> "/" "" -> "/" value -> value end