From e75abcb153160520bf277eb589ca2abbc2b3c49c Mon Sep 17 00:00:00 2001 From: Yongjin Chong Date: Wed, 29 Oct 2025 14:37:20 -0600 Subject: [PATCH 01/15] Secure patch (#175) - Fix OAuth 404 error handling - Limit 10-30 times request per IP address - Inprove routing match sequence --- .../oauth_provider/authorize_controller.ex | 7 ++++ .../oauth_provider/token_controller.ex | 36 +++++++++++++++++++ lib/recognizer_web/router.ex | 22 +++++++----- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/lib/recognizer_web/controllers/oauth_provider/authorize_controller.ex b/lib/recognizer_web/controllers/oauth_provider/authorize_controller.ex index bb6c4992..26ebe948 100644 --- a/lib/recognizer_web/controllers/oauth_provider/authorize_controller.ex +++ b/lib/recognizer_web/controllers/oauth_provider/authorize_controller.ex @@ -4,6 +4,13 @@ defmodule RecognizerWeb.OauthProvider.AuthorizeController do alias ExOauth2Provider.Authorization alias RecognizerWeb.Authentication + @one_minute 60_000 + + # Rate limit: 20 requests per minute per IP for OAuth authorization flow + plug Hammer.Plug, + rate_limit: {"oauth:authorize", @one_minute, 20}, + by: {:conn, &RecognizerWeb.OauthProvider.TokenController.get_remote_ip/1} + def show(conn, %{"code" => code}) do render(conn, "show.html", code: code) end diff --git a/lib/recognizer_web/controllers/oauth_provider/token_controller.ex b/lib/recognizer_web/controllers/oauth_provider/token_controller.ex index e6731c23..17155fff 100644 --- a/lib/recognizer_web/controllers/oauth_provider/token_controller.ex +++ b/lib/recognizer_web/controllers/oauth_provider/token_controller.ex @@ -3,6 +3,32 @@ defmodule RecognizerWeb.OauthProvider.TokenController do alias ExOauth2Provider.Token + @one_minute 60_000 + + # Rate limit: 30 requests per minute per IP for token endpoint + plug Hammer.Plug, + [ + rate_limit: {"oauth:token", @one_minute, 30}, + by: {:conn, &__MODULE__.get_remote_ip/1} + ] + when action in [:create] + + # Rate limit: 10 requests per minute per IP for invalid endpoints + plug Hammer.Plug, + [ + rate_limit: {"oauth:invalid", @one_minute, 10}, + by: {:conn, &__MODULE__.get_remote_ip/1} + ] + when action in [:not_found, :method_not_allowed] + + def get_remote_ip(conn) do + # Get real IP from X-Forwarded-For or remote IP + case Plug.Conn.get_req_header(conn, "x-forwarded-for") do + [ip | _] -> ip |> String.split(",") |> List.first() |> String.trim() + [] -> conn.remote_ip |> :inet.ntoa() |> to_string() + end + end + def create(conn, params) do case Token.grant(params, otp_app: :recognizer) do {:ok, access_token} -> @@ -25,4 +51,14 @@ defmodule RecognizerWeb.OauthProvider.TokenController do "error_description" => "The token endpoint only supports POST" }) end + + # Handle non-existent OAuth endpoints + def not_found(conn, _params) do + conn + |> put_status(:not_found) + |> json(%{ + "error" => "invalid_request", + "error_description" => "The requested OAuth endpoint does not exist" + }) + end end diff --git a/lib/recognizer_web/router.ex b/lib/recognizer_web/router.ex index d5cad9af..514397e8 100644 --- a/lib/recognizer_web/router.ex +++ b/lib/recognizer_web/router.ex @@ -65,11 +65,24 @@ defmodule RecognizerWeb.Router do get "/logout", Accounts.UserSessionController, :delete end + # OAuth authorization flow (browser-based) - must come BEFORE catch-all + scope "/", RecognizerWeb.OauthProvider, as: :oauth do + pipe_through [:browser, :bc, :auth, :user] + + get "/oauth/authorize", AuthorizeController, :new + get "/oauth/authorize/:code", AuthorizeController, :show + post "/oauth/authorize", AuthorizeController, :create + delete "/oauth/authorize", AuthorizeController, :delete + end + + # OAuth token endpoint and catch-all for invalid OAuth paths scope "/", RecognizerWeb.OauthProvider, as: :oauth do pipe_through [:api] post "/oauth/token", TokenController, :create match :*, "/oauth/token", TokenController, :method_not_allowed + # Catch-all for any other /oauth/* paths (security: prevents endpoint scanning) + match :*, "/oauth/*path", TokenController, :not_found end scope "/api", RecognizerWeb.Accounts.Api, as: :api do @@ -84,15 +97,6 @@ defmodule RecognizerWeb.Router do post "/create-account", UserRegistrationController, :create end - scope "/", RecognizerWeb.OauthProvider, as: :oauth do - pipe_through [:browser, :bc, :auth, :user] - - get "/oauth/authorize", AuthorizeController, :new - get "/oauth/authorize/:code", AuthorizeController, :show - post "/oauth/authorize", AuthorizeController, :create - delete "/oauth/authorize", AuthorizeController, :delete - end - scope "/", RecognizerWeb.Accounts do pipe_through [:browser, :bc, :auth, :guest] From c12faf7bca801e7853b21cc02b43582640068a8b Mon Sep 17 00:00:00 2001 From: Yongjin Chong Date: Wed, 29 Oct 2025 15:16:55 -0600 Subject: [PATCH 02/15] Use client_id in OAuth token rate limiting (#176) * Fix OAuth 404 error handling * Limit 10-30 times request per IP address Inprove routing match sequence * Use client_id in OAuth token rate limiting - Rate limit by IP+client_id instead of IP only - Gives each OAuth app independent 100/min quota - Prevents cross-service rate limit interference --- .../oauth_provider/token_controller.ex | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/recognizer_web/controllers/oauth_provider/token_controller.ex b/lib/recognizer_web/controllers/oauth_provider/token_controller.ex index 17155fff..a16ce9b1 100644 --- a/lib/recognizer_web/controllers/oauth_provider/token_controller.ex +++ b/lib/recognizer_web/controllers/oauth_provider/token_controller.ex @@ -5,11 +5,12 @@ defmodule RecognizerWeb.OauthProvider.TokenController do @one_minute 60_000 - # Rate limit: 30 requests per minute per IP for token endpoint + # Rate limit: 100 requests per minute per IP+client combination for token endpoint + # This ensures different OAuth applications don't share the same rate limit bucket plug Hammer.Plug, [ - rate_limit: {"oauth:token", @one_minute, 30}, - by: {:conn, &__MODULE__.get_remote_ip/1} + rate_limit: {"oauth:token", @one_minute, 100}, + by: {:conn, &__MODULE__.get_rate_limit_key/1} ] when action in [:create] @@ -29,6 +30,18 @@ defmodule RecognizerWeb.OauthProvider.TokenController do end end + def get_rate_limit_key(conn) do + ip = get_remote_ip(conn) + client_id = get_client_id_from_params(conn) + "#{ip}:#{client_id}" + end + + defp get_client_id_from_params(conn) do + # OAuth token requests include client_id in params + # This allows different OAuth applications to have separate rate limit buckets + conn.params["client_id"] || "unknown" + end + def create(conn, params) do case Token.grant(params, otp_app: :recognizer) do {:ok, access_token} -> From 2bee3ae2d2830d917d618f7481c68938568bb74e Mon Sep 17 00:00:00 2001 From: Yongjin Chong Date: Thu, 30 Oct 2025 11:32:57 -0600 Subject: [PATCH 03/15] Fix router issue (#177) * Fix OAuth 404 error handling * Limit 10-30 times request per IP address Inprove routing match sequence * Use client_id in OAuth token rate limiting - Rate limit by IP+client_id instead of IP only - Gives each OAuth app independent 100/min quota - Prevents cross-service rate limit interference * Fix OAuth callback blocking by catch-all route Critical fix: Move catch-all route to END of router - /oauth/:provider/callback was being blocked - Customers couldn't log in via GitHub/Google OAuth - Catch-all must come AFTER all legitimate routes Fixes: GitHub OAuth login returning 404 error --- lib/recognizer_web/router.ex | 41 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/recognizer_web/router.ex b/lib/recognizer_web/router.ex index 514397e8..33d66df7 100644 --- a/lib/recognizer_web/router.ex +++ b/lib/recognizer_web/router.ex @@ -65,26 +65,6 @@ defmodule RecognizerWeb.Router do get "/logout", Accounts.UserSessionController, :delete end - # OAuth authorization flow (browser-based) - must come BEFORE catch-all - scope "/", RecognizerWeb.OauthProvider, as: :oauth do - pipe_through [:browser, :bc, :auth, :user] - - get "/oauth/authorize", AuthorizeController, :new - get "/oauth/authorize/:code", AuthorizeController, :show - post "/oauth/authorize", AuthorizeController, :create - delete "/oauth/authorize", AuthorizeController, :delete - end - - # OAuth token endpoint and catch-all for invalid OAuth paths - scope "/", RecognizerWeb.OauthProvider, as: :oauth do - pipe_through [:api] - - post "/oauth/token", TokenController, :create - match :*, "/oauth/token", TokenController, :method_not_allowed - # Catch-all for any other /oauth/* paths (security: prevents endpoint scanning) - match :*, "/oauth/*path", TokenController, :not_found - end - scope "/api", RecognizerWeb.Accounts.Api, as: :api do pipe_through [:api, :auth, :user] @@ -147,4 +127,25 @@ defmodule RecognizerWeb.Router do post "/settings/two-factor", UserSettingsController, :two_factor_confirm get "/setting/two-factor/resend", UserSettingsController, :resend end + + # OAuth Provider endpoints (for other services using Recognizer as OAuth provider) + scope "/", RecognizerWeb.OauthProvider, as: :oauth do + pipe_through [:browser, :bc, :auth, :user] + + get "/oauth/authorize", AuthorizeController, :new + get "/oauth/authorize/:code", AuthorizeController, :show + post "/oauth/authorize", AuthorizeController, :create + delete "/oauth/authorize", AuthorizeController, :delete + end + + # OAuth Provider token endpoint and catch-all for attack prevention + scope "/", RecognizerWeb.OauthProvider, as: :oauth do + pipe_through [:api] + + post "/oauth/token", TokenController, :create + match :*, "/oauth/token", TokenController, :method_not_allowed + # Catch-all for invalid OAuth provider paths (e.g. /oauth/.env, /oauth/auth.json) + # Note: This must come AFTER all legitimate /oauth/* routes to avoid blocking them + match :*, "/oauth/*path", TokenController, :not_found + end end From f656976d9a107947a6c333ce27044adc52662a17 Mon Sep 17 00:00:00 2001 From: Yongjin Chong Date: Thu, 30 Oct 2025 18:48:11 -0600 Subject: [PATCH 04/15] Fix oauth security minimal (#178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add rate limiting and security to OAuth endpoints - Add rate limiting to all OAuth endpoints to prevent abuse - Token endpoint: 100/min per IP+client_id (independent quotas per OAuth client) - Authorize endpoint: 50/min per IP - User OAuth (GitHub/Google): 20/min per IP - Invalid paths: 10/min per IP - Add catch-all route for scanning attacks (/oauth/.env, etc.) - Support X-Forwarded-For for load balancers Fixes Datadog errors from endpoint scanning attacks. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Revert "Login sync bc (#174)" This reverts commit 09700602fa5d99003b613e883e73ad31ec1e883e. * Revert "Update Oauth account can't recevice password changing email (#173)" This reverts commit 3b7cd90706311b76fd02a4a1e50d9f20af7fd5e3. * Revert "Redirect 405 error (#172)" This reverts commit b16b416da0414a311d897f933a2ef515e541f941. * Revert "Fix BigCommerce integration to prevent incomplete account creation (#171)" This reverts commit d37bde6306be424fb9973882bc132055286a48d9. * Revert "Update email login cases - only 422 email exist case can success now (#170)" This reverts commit f836a1eb4a4f2c15ba65ba96187a6b834c6b4f18. * Revert "Fix deploy issue (#169)" This reverts commit 1c25838d5651d3e7a6b5e798914ab9e64ed81334. * Revert "Add rate limiting and security to OAuth endpoints" This reverts commit 0a65c212d6997afd57f84f593b72b80daee07605. --------- Co-authored-by: Claude --- config/prod.exs | 1 + config/releases.exs | 2 +- lib/recognizer/accounts.ex | 94 +++------- lib/recognizer/bigcommerce.ex | 141 +++----------- lib/recognizer/bigcommerce/client.ex | 51 +---- lib/recognizer/hal.ex | 44 ++--- lib/recognizer_web/authentication.ex | 59 +----- .../accounts/prompt/two_factor_controller.ex | 22 +-- .../accounts/user_oauth_controller.ex | 3 +- .../accounts/user_session_controller.ex | 4 +- .../accounts/user_settings_controller.ex | 177 +++++------------- mix.exs | 13 +- 12 files changed, 127 insertions(+), 484 deletions(-) diff --git a/config/prod.exs b/config/prod.exs index 846b8f61..b5fec7a3 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,6 +1,7 @@ import Config config :recognizer, RecognizerWeb.Endpoint, + url: [scheme: "https", port: 443], http: [port: 8080], cache_static_manifest: "priv/static/cache_manifest.json", gzip: true, diff --git a/config/releases.exs b/config/releases.exs index ca60e2b8..6e2d6756 100644 --- a/config/releases.exs +++ b/config/releases.exs @@ -11,7 +11,7 @@ config :recognizer, hal_token: recognizer_config["HAL_TOKEN"] config :recognizer, RecognizerWeb.Endpoint, - url: [scheme: "https", port: 443, host: recognizer_config["DOMAIN"]], + url: [host: recognizer_config["DOMAIN"]], secret_key_base: recognizer_config["SECRET_KEY_BASE"] config :recognizer, Recognizer.Repo, diff --git a/lib/recognizer/accounts.ex b/lib/recognizer/accounts.ex index 4a42e1c8..158d279e 100644 --- a/lib/recognizer/accounts.ex +++ b/lib/recognizer/accounts.ex @@ -226,12 +226,7 @@ defmodule Recognizer.Accounts do {:error, error} -> # Log the error but continue with account creation - # Auto-sync during login will retry the synchronization - Logger.error( - "[BigCommerce Sync] βœ— REGISTRATION SYNC FAILED for user #{user.id} (#{user.email}) - " <> - "Reason: #{inspect(error)} - Will retry on next login" - ) - + Logger.error("BigCommerce customer creation failed but continuing account process: #{inspect(error)}") {:ok, user} end else @@ -252,48 +247,30 @@ defmodule Recognizer.Accounts do error end - defp maybe_send_newsletter_after_registration({:ok, user} = previous_response, attrs) do - newsletter_value = normalize_newsletter_value(Map.get(attrs, "newsletter")) - if newsletter_value, do: start_newsletter_update_task(user, true) - previous_response - end - - defp maybe_send_newsletter_after_registration(previous_response, _attrs) do - previous_response - end - - defp normalize_newsletter_value(value) when value in [true, "true"], do: true - defp normalize_newsletter_value(value) when value in [false, "false", nil], do: false - defp normalize_newsletter_value(_), do: false - - defp start_newsletter_update_task(user, newsletter_value) do + defp maybe_send_newsletter_after_registration({:ok, user} = previous_response, %{"newsletter" => "true"}) do + # Process asynchronously to avoid blocking the account creation if newsletter registration fails Task.start(fn -> try do - user_with_newsletter = Map.put(user, :newsletter, newsletter_value) - result = Recognizer.Hal.update_newsletter(user_with_newsletter) - log_newsletter_result(user.id, result) + require Logger + result = Recognizer.Hal.update_newsletter(user) + Logger.info("Newsletter registration completed for user #{user.id}: #{inspect(result)}") catch kind, reason -> - Logger.error("Newsletter update crashed for user #{user.id}: #{inspect(kind)}, #{inspect(reason)}") + require Logger + Logger.error("Newsletter registration failed for user #{user.id}: #{inspect(kind)}, #{inspect(reason)}") Logger.error(Exception.format_stacktrace(__STACKTRACE__)) end end) - end - defp log_newsletter_result(user_id, result) do - case result do - :ok -> - Logger.info("Newsletter update successful for user #{user_id}") - - :ok_not_updated -> - Logger.info("Newsletter already up to date for user #{user_id}") + previous_response + end - {:error, reason} -> - Logger.warn("Newsletter update failed for user #{user_id}: #{inspect(reason)}") + defp maybe_send_newsletter_after_registration(previous_response, %{"newsletter" => false}) do + previous_response + end - other -> - Logger.debug("Newsletter update returned: #{inspect(other)} for user #{user_id}") - end + defp maybe_send_newsletter_after_registration(previous_response, _attrs) do + previous_response end @doc """ @@ -338,10 +315,7 @@ defmodule Recognizer.Accounts do """ def update_user(user, attrs) do - if Map.has_key?(attrs, "newsletter") do - start_newsletter_update_task(user, normalize_newsletter_value(Map.get(attrs, "newsletter"))) - end - + if Map.has_key?(attrs, "newsletter"), do: Recognizer.Hal.update_newsletter(attrs) changeset = User.changeset(user, attrs) with {:ok, updated_user} <- Repo.update(changeset), @@ -508,9 +482,6 @@ defmodule Recognizer.Accounts do @doc """ Delivers the reset password email to the given user. - For security reasons, if the user account is OAuth-only (no password set), - we silently skip sending the email to prevent account enumeration attacks. - ## Examples iex> deliver_user_reset_password_instructions(user, &Routes.user_reset_password_url(conn, :edit, &1)) @@ -519,23 +490,12 @@ defmodule Recognizer.Accounts do """ def deliver_user_reset_password_instructions(%User{} = user, reset_password_url_fun) when is_function(reset_password_url_fun, 1) do - # Preload OAuth associations to check if this is an OAuth-only account - user_with_oauths = Repo.preload(user, :oauths) - - if Enum.any?(user_with_oauths.oauths) do - # OAuth account - silently skip sending email for security - # Return success to prevent account enumeration - Logger.info("Password reset requested for OAuth-only account #{user.id}, skipping email") - {:ok, :skipped} - else - # Regular password account - send reset email - {:ok, token, _claims} = Guardian.encode_and_sign(user, %{"typ" => "reset_password"}) + {:ok, token, _claims} = Guardian.encode_and_sign(user, %{"typ" => "reset_password"}) - Notification.deliver_reset_password_instructions( - user, - reset_password_url_fun.(token) - ) - end + Notification.deliver_reset_password_instructions( + user, + reset_password_url_fun.(token) + ) end @doc """ @@ -692,16 +652,8 @@ defmodule Recognizer.Accounts do settings. """ def check_two_factor_notification_time(user) do - case get_new_two_factor_settings(user) do - {:ok, attrs} when not is_nil(attrs) -> - check_two_factor_notification_time(attrs, 100) - - {:ok, nil} -> - {:error, :no_two_factor_settings} - - {:error, reason} -> - {:error, reason} - end + {:ok, attrs} = get_new_two_factor_settings(user) + check_two_factor_notification_time(attrs, 100) end def check_two_factor_notification_time(attrs, two_factor_issue_time) do diff --git a/lib/recognizer/bigcommerce.ex b/lib/recognizer/bigcommerce.ex index 75a1f61f..0ad6afcc 100644 --- a/lib/recognizer/bigcommerce.ex +++ b/lib/recognizer/bigcommerce.ex @@ -16,157 +16,70 @@ defmodule Recognizer.BigCommerce do def create_customer(user) do case Client.create_customer(user) do - {:ok, :email_already_exists} -> - handle_email_already_exists(user) - - {:ok, bc_id} when is_integer(bc_id) -> - handle_new_customer_created(user, bc_id) - - {:error, e} -> - handle_customer_creation_error(user, e) - end - end - - defp handle_email_already_exists(user) do - Logger.warn( - "[BigCommerce Sync] Email already exists in BC for user #{user.id} (#{user.email}), attempting to link existing customer" - ) - - case Client.get_customers(emails: [user.email]) do - {:ok, [customer_id | _]} -> - link_existing_customer(user, customer_id) - - {:ok, []} -> - handle_customer_not_found_error(user) - - {:error, e} -> - handle_get_customers_error(user, e) - end - end - - defp link_existing_customer(user, customer_id) do - case Repo.insert(%Customer{user_id: user.id, bc_id: customer_id}) do - {:ok, _} -> - Logger.warn( - "[BigCommerce Sync] βœ“ Successfully linked existing BC customer #{customer_id} to user #{user.id} (#{user.email})" - ) - - {:ok, user} - - {:error, changeset} -> - Logger.error( - "[BigCommerce Sync] βœ— FAILED to link BC customer #{customer_id} to user #{user.id} (#{user.email}) - " <> - "Changeset errors: #{inspect(changeset.errors)}" - ) - - {:error, {:bigcommerce_link_failed, changeset}} - end - end - - defp handle_customer_not_found_error(user) do - Logger.error( - "[BigCommerce Sync] βœ— CRITICAL: BC reported email exists but customer NOT FOUND via API " <> - "for user #{user.id} (#{user.email}) - Possible BC API inconsistency" - ) - - {:error, {:bigcommerce_customer_not_found, "Email exists but customer not found"}} - end - - defp handle_get_customers_error(user, e) do - Logger.error( - "[BigCommerce Sync] βœ— BC API ERROR while fetching customer for user #{user.id} (#{user.email}) - " <> - "Error: #{inspect(e)}" - ) - - {:error, {:bigcommerce_api_error, e}} - end - - defp handle_new_customer_created(user, bc_id) do - case Repo.insert(%Customer{user_id: user.id, bc_id: bc_id}) do - {:ok, _} -> - Logger.warn("[BigCommerce Sync] βœ“ Created NEW BC customer #{bc_id} for user #{user.id} (#{user.email})") - + {:ok, bc_id} -> + Repo.insert(%Customer{user_id: user.id, bc_id: bc_id}) {:ok, user} - {:error, changeset} -> - Logger.error( - "[BigCommerce Sync] βœ— CRITICAL: BC customer #{bc_id} created but DB LINK FAILED " <> - "for user #{user.id} (#{user.email}) - Changeset: #{inspect(changeset.errors)}" - ) - - {:error, {:bigcommerce_link_failed, changeset}} + {:error, e} -> + Logger.error("error creating bigcommerce customer: #{inspect(e)}") + {:error, e} end end - defp handle_customer_creation_error(user, e) do - Logger.error( - "[BigCommerce Sync] βœ— BC customer creation FAILED for user #{user.id} (#{user.email}) - " <> - "Error: #{inspect(e)}" - ) - - {:error, e} - end - def get_or_create_customer(%{email: email, id: id} = user) do - Logger.info("[BigCommerce Sync] Starting sync for user #{id} (#{email})") + Logger.info("Starting BigCommerce get_or_create_customer for user #{id} with email #{email}") case Client.get_customers(emails: [email]) do {:ok, []} -> - Logger.info("[BigCommerce Sync] No existing BC customer found for #{email}, creating new...") + Logger.info("No existing BigCommerce customer found for email #{email}, creating new customer") result = create_customer(user) - Logger.info("[BigCommerce Sync] Creation result: #{inspect(result)}") + Logger.info("BigCommerce customer creation result: #{inspect(result)}") result {:ok, [customer_id]} -> - Logger.info("[BigCommerce Sync] Found existing BC customer #{customer_id} for #{email}") + Logger.info("Found existing BigCommerce customer #{customer_id} for email #{email}") case Repo.insert(%Customer{user_id: id, bc_id: customer_id}) do {:ok, _customer_db_entry} -> - Logger.warn("[BigCommerce Sync] βœ“ Linked BC customer #{customer_id} to user #{id} (#{email})") - + Logger.info("Successfully linked BigCommerce customer #{customer_id} to user #{id}") {:ok, user} {:error, changeset} -> - Logger.error( - "[BigCommerce Sync] βœ— DB INSERT FAILED for user #{id} (#{email}) - " <> - "Changeset: #{inspect(changeset.errors)}" - ) - - # Apply strict approach: fail account creation when DB linking fails - {:error, {:bigcommerce_link_failed, changeset}} + Logger.error("Error inserting BigCommerce customer into local DB: #{inspect(changeset)}") + # Return success anyway since the BigCommerce customer exists + # This helps with the case where a user tries to create an account twice + Logger.info("Returning success despite DB error since BigCommerce customer exists") + {:ok, user} end {:error, e} -> - Logger.error("[BigCommerce Sync] βœ— BC API ERROR for user #{id} (#{email}) - Error: #{inspect(e)}") - - # Apply strict approach: fail account creation for BigCommerce API errors - {:error, e} + Logger.error("Error while getting BigCommerce customer: #{inspect(e)}") + # Don't fail account creation due to BigCommerce API errors + # This ensures verification emails are still sent + Logger.info("Continuing account creation process despite BigCommerce error") + {:ok, user} e -> - Logger.error("[BigCommerce Sync] βœ— UNEXPECTED ERROR for user #{id} (#{email}) - Error: #{inspect(e)}") - - # Apply strict approach: fail account creation for unexpected errors - {:error, "Unexpected BigCommerce error"} + Logger.error("Unexpected error while getting or creating BigCommerce customer: #{inspect(e)}") + # Don't fail account creation due to BigCommerce errors + # This ensures verification emails are still sent + Logger.info("Continuing account creation process despite unexpected BigCommerce error") + {:ok, user} end end def get_or_create_customer(e) do - Logger.error("[BigCommerce Sync] βœ— INVALID INPUT: #{inspect(e)}") + Logger.error("unexpected customer #{inspect(e)}") {:error, "unexpected customer"} end def update_customer(user) do case Client.update_customer(Repo.preload(user, :bigcommerce_user)) do - {:ok, :email_already_exists} -> - # When updating, email already exists is actually a success condition - Logger.info("BigCommerce customer update: email already exists for user #{user.id}, treating as success") - {:ok, user} - {:ok, _} -> {:ok, user} {:error, e} -> - Logger.error("BigCommerce customer update failed: #{inspect(e)}") + Logger.error("error creating bigcommerce customer: #{inspect(e)}") {:error, e} end end diff --git a/lib/recognizer/bigcommerce/client.ex b/lib/recognizer/bigcommerce/client.ex index 7a6e54c9..f89165aa 100644 --- a/lib/recognizer/bigcommerce/client.ex +++ b/lib/recognizer/bigcommerce/client.ex @@ -48,9 +48,6 @@ defmodule Recognizer.BigCommerce.Client do {:ok, %Response{body: response, status_code: 200}} -> {:ok, response} - {:ok, %Response{status_code: 422, body: body}} -> - handle_422_error(body, "create") - {:ok, %Response{status_code: 429, headers: headers}} -> sleep_for_rate_limit(headers) post_customer(customer_json) @@ -68,9 +65,6 @@ defmodule Recognizer.BigCommerce.Client do {:ok, %Response{body: response, status_code: 200}} -> {:ok, response} - {:ok, %Response{status_code: 422, body: body}} -> - handle_422_error(body, "update") - {:ok, %Response{status_code: 429, headers: headers}} -> sleep_for_rate_limit(headers) put_customer(customer_json) @@ -83,36 +77,6 @@ defmodule Recognizer.BigCommerce.Client do end end - # Precisely analyze 422 errors to determine if it's email duplication - defp handle_422_error(body, operation) do - case Jason.decode(body) do - {:ok, %{"errors" => %{".customer_update" => error}}} when is_binary(error) -> - if String.contains?(String.downcase(error), "email") and - String.contains?(String.downcase(error), "already in use") do - Logger.info("BigCommerce customer #{operation}: email already exists, treating as success - #{error}") - {:ok, :email_already_exists} - else - Logger.error("BigCommerce customer #{operation} failed with validation error: #{error}") - {:error, {:validation_error, error}} - end - - {:ok, %{"errors" => errors}} -> - Logger.error("BigCommerce customer #{operation} failed with errors: #{inspect(errors)}") - {:error, {:validation_errors, errors}} - - {:ok, decoded} -> - Logger.error("BigCommerce customer #{operation} failed with unexpected 422 response: #{inspect(decoded)}") - {:error, {:unexpected_422, decoded}} - - {:error, json_error} -> - Logger.error( - "BigCommerce customer #{operation} failed with unparseable 422 response: #{body}, JSON error: #{inspect(json_error)}" - ) - - {:error, {:unparseable_422, body}} - end - end - def get_customers(queries \\ []) do with params <- customer_queries_as_params(queries), full_uri <- customers_uri(), @@ -161,17 +125,10 @@ defmodule Recognizer.BigCommerce.Client do end defp get_id(response) do - case response do - :email_already_exists -> - # If email already exists, return special response for upper level handling - {:ok, :email_already_exists} - - _ -> - case Jason.decode(response) do - {:ok, %{"data" => [%{"id" => id}]}} -> {:ok, id} - {:error, e} -> {:error, e} - e -> {:error, e} - end + case Jason.decode(response) do + {:ok, %{"data" => [%{"id" => id}]}} -> {:ok, id} + {:error, e} -> {:error, e} + e -> {:error, e} end end diff --git a/lib/recognizer/hal.ex b/lib/recognizer/hal.ex index 733a13d1..5f810031 100644 --- a/lib/recognizer/hal.ex +++ b/lib/recognizer/hal.ex @@ -44,24 +44,16 @@ defmodule Recognizer.Hal do end # Validate user data - defp validate_user_data(%{email: nil} = user) do - Logger.error("update_newsletter/1 called with user missing email: #{inspect(user)}") - {:error, :missing_email} - end - - defp validate_user_data(%{} = user) do - newsletter = - case Map.get(user, :newsletter) do - nil -> false - v -> v - end - - {:ok, Map.put(user, :newsletter, newsletter)} - end - defp validate_user_data(user) do - Logger.error("update_newsletter/1 called with non-map user data: #{inspect(user)}") - {:error, :invalid_user_data} + if is_map(user) && !is_nil(Map.get(user, :email)) && !is_nil(Map.get(user, :newsletter)) do + {:ok, user} + else + Logger.error( + "update_newsletter/1 called with invalid user data (missing :email or :newsletter field): #{inspect(user)}" + ) + + {:error, :invalid_user_data} + end end # Fetch data with retry mechanism @@ -120,22 +112,10 @@ defmodule Recognizer.Hal do interests = if is_list(raw_interests), do: raw_interests, else: [] newsletter_status_value = Map.get(user, :newsletter) - # Convert boolean to HAL API expected format - # HAL API expects boolean values for interest groups subscription status - status = - case newsletter_status_value do - # User wants to subscribe - true -> true - # User doesn't want to subscribe - false -> false - # Default to unsubscribed if nil - nil -> false - end - - Logger.debug("Setting newsletter interests for user #{Map.get(user, :email)}: all interests set to #{status}") - Enum.reduce(interests, %{}, fn item, acc -> - Map.put(acc, item["id"], status) + atom_value = newsletter_status_value + + Map.put(acc, item["id"], atom_value) end) end diff --git a/lib/recognizer_web/authentication.ex b/lib/recognizer_web/authentication.ex index b4c16933..5ec98272 100644 --- a/lib/recognizer_web/authentication.ex +++ b/lib/recognizer_web/authentication.ex @@ -31,10 +31,7 @@ defmodule RecognizerWeb.Authentication do |> put_session(:prompt_user_id, user.id) |> redirect(to: Routes.prompt_two_factor_path(conn, :new)) - {:ok, user} -> - # Attempt to sync BigCommerce in background if not already synced - ensure_bigcommerce_user_async(user) - + {:ok, _user} -> redirect_opts = login_redirect(conn, user) conn @@ -82,60 +79,6 @@ defmodule RecognizerWeb.Authentication do Guardian.Plug.current_resource(conn) end - @doc """ - Sets up two-factor authentication session for a user. - This is used across multiple controllers to maintain consistency. - """ - def put_two_factor_session(conn, user) do - conn - |> put_session(:two_factor_user_id, user.id) - |> put_session(:two_factor_sent, false) - |> put_session(:two_factor_issue_time, System.system_time(:second)) - end - - @doc """ - Ensures BigCommerce user synchronization in the background. - This does not block the login flow if sync fails. - """ - if Mix.env() == :test do - def ensure_bigcommerce_user_async(_user), do: :noop - else - def ensure_bigcommerce_user_async(user) do - unless should_sync_on_login?(), do: :noop - - user = Recognizer.Repo.preload(user, :bigcommerce_user) - - if is_nil(user.bigcommerce_user) do - Task.start(fn -> sync_bigcommerce_customer(user) end) - end - end - end - - defp should_sync_on_login? do - BigCommerce.enabled?() and auto_sync_enabled?() - end - - defp auto_sync_enabled? do - Application.get_env(:recognizer, Recognizer.BigCommerce) - |> Keyword.get(:auto_sync_on_login?, true) - end - - defp sync_bigcommerce_customer(user) do - require Logger - - Logger.warn("[BigCommerce Sync] Attempting auto-sync for user #{user.id} (#{user.email}) during login") - - case BigCommerce.get_or_create_customer(user) do - {:ok, _user} -> - Logger.warn("[BigCommerce Sync] βœ“ Successfully synced user #{user.id} (#{user.email}) - BC account linked") - - {:error, reason} -> - Logger.error( - "[BigCommerce Sync] βœ— FAILED to sync user #{user.id} (#{user.email}) - Reason: #{inspect(reason)} - USER MAY NOT BE ABLE TO PLACE ORDERS" - ) - end - end - @doc """ The URL to redirect the user to after authentication is done. """ diff --git a/lib/recognizer_web/controllers/accounts/prompt/two_factor_controller.ex b/lib/recognizer_web/controllers/accounts/prompt/two_factor_controller.ex index 526d8cfa..c4942adc 100644 --- a/lib/recognizer_web/controllers/accounts/prompt/two_factor_controller.ex +++ b/lib/recognizer_web/controllers/accounts/prompt/two_factor_controller.ex @@ -25,24 +25,12 @@ defmodule RecognizerWeb.Accounts.Prompt.TwoFactorController do def edit(conn, _params) do user = conn.assigns.user + {:ok, %{two_factor_seed: seed}} = Accounts.get_new_two_factor_settings(user) - case Accounts.get_new_two_factor_settings(user) do - {:ok, %{two_factor_seed: seed}} -> - render(conn, "confirm.html", - barcode: Authentication.generate_totp_barcode(user, seed), - totp_app_url: Authentication.get_totp_app_url(user, seed) - ) - - {:ok, nil} -> - conn - |> put_flash(:error, "Two factor setup not found. Please set up two factor authentication first.") - |> redirect(to: Routes.prompt_two_factor_path(conn, :new)) - - {:error, _reason} -> - conn - |> put_flash(:error, "Error retrieving two factor settings. Please try again.") - |> redirect(to: Routes.prompt_two_factor_path(conn, :new)) - end + render(conn, "confirm.html", + barcode: Authentication.generate_totp_barcode(user, seed), + totp_app_url: Authentication.get_totp_app_url(user, seed) + ) end def update(conn, params) do diff --git a/lib/recognizer_web/controllers/accounts/user_oauth_controller.ex b/lib/recognizer_web/controllers/accounts/user_oauth_controller.ex index 26461b4d..407ab1f7 100644 --- a/lib/recognizer_web/controllers/accounts/user_oauth_controller.ex +++ b/lib/recognizer_web/controllers/accounts/user_oauth_controller.ex @@ -25,7 +25,8 @@ defmodule RecognizerWeb.Accounts.UserOAuthController do {:two_factor, user} -> conn - |> Authentication.put_two_factor_session(user) + |> put_session(:two_factor_user_id, user.id) + |> put_session(:two_factor_sent, false) |> redirect(to: Routes.user_two_factor_path(conn, :new)) {:error, %Ecto.Changeset{} = changeset} -> diff --git a/lib/recognizer_web/controllers/accounts/user_session_controller.ex b/lib/recognizer_web/controllers/accounts/user_session_controller.ex index 17bd3cf3..51d0777b 100644 --- a/lib/recognizer_web/controllers/accounts/user_session_controller.ex +++ b/lib/recognizer_web/controllers/accounts/user_session_controller.ex @@ -17,7 +17,9 @@ defmodule RecognizerWeb.Accounts.UserSessionController do {:two_factor, user} -> conn - |> Authentication.put_two_factor_session(user) + |> put_session(:two_factor_user_id, user.id) + |> put_session(:two_factor_sent, false) + |> put_session(:two_factor_issue_time, System.system_time(:second)) |> redirect(to: Routes.user_two_factor_path(conn, :new)) {:oauth, _user} -> diff --git a/lib/recognizer_web/controllers/accounts/user_settings_controller.ex b/lib/recognizer_web/controllers/accounts/user_settings_controller.ex index 6cc207da..606c5dd4 100644 --- a/lib/recognizer_web/controllers/accounts/user_settings_controller.ex +++ b/lib/recognizer_web/controllers/accounts/user_settings_controller.ex @@ -48,162 +48,79 @@ defmodule RecognizerWeb.Accounts.UserSettingsController do """ def two_factor_init(conn, _params) do user = Authentication.fetch_current_user(conn) + current_user = Accounts.get_new_two_factor_settings(user) + {:ok, %{two_factor_seed: seed, notification_preference: %{two_factor: method}} = setting_user} = current_user - case Accounts.get_new_two_factor_settings(user) do - {:ok, nil} -> - handle_missing_two_factor_settings(conn) - - {:ok, %{two_factor_seed: seed, notification_preference: %{two_factor: method}} = setting_user} -> - handle_two_factor_init_with_settings(conn, user, setting_user, seed, method) - - {:error, _reason} -> - handle_two_factor_settings_error(conn) - - _ -> - handle_invalid_two_factor_format(conn) - end - end - - defp handle_missing_two_factor_settings(conn) do - conn - |> put_flash( - :error, - "Two factor setup expired or not yet initiated. Please enable two factor authentication first." - ) - |> redirect(to: Routes.user_settings_path(conn, :edit)) - end - - defp handle_two_factor_init_with_settings(conn, user, setting_user, seed, method) do method_atom = normalize_to_atom(method) if method in [:app, "app"] do - render_app_two_factor(conn, user, seed) - else - render_external_two_factor(conn, setting_user, user, method_atom) - end - end - - defp render_app_two_factor(conn, user, seed) do - render(conn, "confirm_two_factor.html", - barcode: Authentication.generate_totp_barcode(user, seed), - totp_app_url: Authentication.get_totp_app_url(user, seed) - ) - end - - defp render_external_two_factor(conn, setting_user, user, method_atom) do - conn = ensure_two_factor_issue_time_session(conn) - conn = maybe_send_two_factor_notification(conn, setting_user, user, method_atom) - render(conn, "confirm_two_factor_external.html") - end - - defp ensure_two_factor_issue_time_session(conn) do - if get_session(conn, :two_factor_issue_time) == nil do - put_session(conn, :two_factor_issue_time, System.system_time(:second)) - else - conn - end - end - - defp maybe_send_two_factor_notification(conn, setting_user, user, method_atom) do - two_factor_sent = get_session(conn, :two_factor_sent) - - if two_factor_sent do - conn + render(conn, "confirm_two_factor.html", + barcode: Authentication.generate_totp_barcode(user, seed), + totp_app_url: Authentication.get_totp_app_url(user, seed) + ) else - conn - |> put_session(:two_factor_sent, true) - |> send_two_factor_notification(setting_user, user, method_atom) + conn = + if get_session(conn, :two_factor_issue_time) == nil do + put_session(conn, :two_factor_issue_time, System.system_time(:second)) + else + conn + end + + two_factor_sent = get_session(conn, :two_factor_sent) + + conn = + if two_factor_sent do + conn + else + conn = put_session(conn, :two_factor_sent, true) + + conn + |> send_two_factor_notification(setting_user, user, method_atom) + end + + render(conn, "confirm_two_factor_external.html") end end - defp handle_two_factor_settings_error(conn) do - conn - |> put_flash(:error, "Error retrieving two factor settings. Please try again.") - |> redirect(to: Routes.user_settings_path(conn, :edit)) - end - - defp handle_invalid_two_factor_format(conn) do - conn - |> put_flash(:error, "Invalid two factor settings format. Please contact support.") - |> redirect(to: Routes.user_settings_path(conn, :edit)) - end - @doc """ Confirming and saving a new two factor setup with user-provided code """ def two_factor_confirm(conn, params) do user = Authentication.fetch_current_user(conn) - two_factor_code = Map.get(params, "two_factor_code") - - case Accounts.get_new_two_factor_settings(user) do - {:ok, nil} -> - handle_missing_two_factor_settings(conn) + current_user = Accounts.get_new_two_factor_settings(user) - {:ok, %{notification_preference: %{two_factor: method}} = setting_user} -> - handle_two_factor_confirm_with_settings(conn, user, setting_user, two_factor_code, method) - - {:error, _reason} -> - handle_two_factor_settings_error(conn) - - _ -> - handle_invalid_two_factor_format(conn) - end - end + two_factor_code = Map.get(params, "two_factor_code") + {:ok, %{notification_preference: %{two_factor: method}} = setting_user} = current_user - defp handle_two_factor_confirm_with_settings(conn, user, setting_user, two_factor_code, method) do current_time = System.system_time(:second) conn = ensure_two_factor_issue_time(conn, current_time) + two_factor_issue_time = get_session(conn, :two_factor_issue_time) method_atom = normalize_to_atom(method) if Authentication.valid_token?(method_atom, two_factor_code, two_factor_issue_time, setting_user.two_factor_seed) do - handle_valid_two_factor_token( - conn, - user, - setting_user, - two_factor_code, - method_atom, - current_time, - two_factor_issue_time - ) - else - handle_invalid_two_factor_token(conn) - end - end + if current_time - two_factor_issue_time > 900 do + conn = + conn + |> put_session(:two_factor_issue_time, current_time) - defp handle_valid_two_factor_token( - conn, - user, - setting_user, - two_factor_code, - method_atom, - current_time, - two_factor_issue_time - ) do - if current_time - two_factor_issue_time > 900 do - handle_expired_two_factor_code(conn, setting_user, user, method_atom, current_time) + conn + |> send_two_factor_notification(setting_user, user, method_atom) + |> put_flash( + :error, + "Two-factor code has expired. A new code has been sent. Please check your email for the newest two-factor code and try again." + ) + |> redirect(to: Routes.user_settings_path(conn, :two_factor_confirm)) + else + handle_two_factor_settings(conn, user, two_factor_code, method_atom) + end else - handle_two_factor_settings(conn, user, two_factor_code, method_atom) + conn + |> put_flash(:error, "Two factor code is invalid") + |> redirect(to: Routes.user_settings_path(conn, :two_factor_confirm)) end end - defp handle_expired_two_factor_code(conn, setting_user, user, method_atom, current_time) do - conn - |> put_session(:two_factor_issue_time, current_time) - |> send_two_factor_notification(setting_user, user, method_atom) - |> put_flash( - :error, - "Two-factor code has expired. A new code has been sent. Please check your email for the newest two-factor code and try again." - ) - |> redirect(to: Routes.user_settings_path(conn, :two_factor_confirm)) - end - - defp handle_invalid_two_factor_token(conn) do - conn - |> put_flash(:error, "Two factor code is invalid") - |> redirect(to: Routes.user_settings_path(conn, :two_factor_confirm)) - end - defp ensure_two_factor_issue_time(conn, current_time) do if get_session(conn, :two_factor_issue_time) == nil do put_session(conn, :two_factor_issue_time, current_time) diff --git a/mix.exs b/mix.exs index 38efc90e..46f8d855 100644 --- a/mix.exs +++ b/mix.exs @@ -10,8 +10,7 @@ defmodule Recognizer.MixProject do compilers: Mix.compilers(), start_permanent: Mix.env() == :prod, aliases: aliases(), - deps: deps(), - releases: releases() + deps: deps() ] end @@ -96,14 +95,4 @@ defmodule Recognizer.MixProject do test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] ] end - - defp releases do - [ - recognizer: [ - validate_compile_env: false, - include_executables_for: [:unix], - applications: [runtime_tools: :permanent] - ] - ] - end end From 672c1d526eb90d8d1fd4f311dbd83a7cda4876ce Mon Sep 17 00:00:00 2001 From: Yongjin Chong Date: Thu, 30 Oct 2025 19:39:32 -0600 Subject: [PATCH 05/15] Add releases configuration for ECS deployment (#179) --- mix.exs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 46f8d855..38efc90e 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,8 @@ defmodule Recognizer.MixProject do compilers: Mix.compilers(), start_permanent: Mix.env() == :prod, aliases: aliases(), - deps: deps() + deps: deps(), + releases: releases() ] end @@ -95,4 +96,14 @@ defmodule Recognizer.MixProject do test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] ] end + + defp releases do + [ + recognizer: [ + validate_compile_env: false, + include_executables_for: [:unix], + applications: [runtime_tools: :permanent] + ] + ] + end end From 38f8571d07893f69152e41be1ee8ae44411a7ca7 Mon Sep 17 00:00:00 2001 From: Yongjin Chong Date: Tue, 4 Nov 2025 09:34:22 -0700 Subject: [PATCH 06/15] Plug.Conn.NotSentError (#180) * Plug.Conn.NotSentError * Plug.Conn.NotSentError --- BIGCOMMERCE_SYNC.md | 163 ++++++++++++++++++ .../accounts/user_oauth_controller.ex | 19 +- 2 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 BIGCOMMERCE_SYNC.md diff --git a/BIGCOMMERCE_SYNC.md b/BIGCOMMERCE_SYNC.md new file mode 100644 index 00000000..eb08f83f --- /dev/null +++ b/BIGCOMMERCE_SYNC.md @@ -0,0 +1,163 @@ +# BigCommerce 계정 동기화 κ°œμ„  + +## κ°œμš” + +이 λ¬Έμ„œλŠ” BigCommerce와 μžμ‚¬ 계정 κ°„ 동기화 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•œ κ°œμ„ μ‚¬ν•­μ„ μ„€λͺ…ν•©λ‹ˆλ‹€. + +## κ΅¬ν˜„λœ κ°œμ„ μ‚¬ν•­ + +### 1. 둜그인 μ‹œ μžλ™ μž¬λ™κΈ°ν™” + +**파일**: `lib/recognizer_web/authentication.ex` + +**κΈ°λŠ₯**: +- μ‚¬μš©μžκ°€ λ‘œκ·ΈμΈν•  λ•Œ BigCommerce 계정이 λ™κΈ°ν™”λ˜μ§€ μ•Šμ€ 경우 μžλ™μœΌλ‘œ λ°±κ·ΈλΌμš΄λ“œμ—μ„œ 동기화 μ‹œλ„ +- 동기화 μ‹€νŒ¨ μ‹œμ—λ„ λ‘œκ·ΈμΈμ€ μ •μƒμ μœΌλ‘œ μ§„ν–‰ (μ‚¬μš©μž κ²½ν—˜μ— 영ν–₯ μ—†μŒ) +- μƒμ„Έν•œ 둜그λ₯Ό 톡해 동기화 성곡/μ‹€νŒ¨ 좔적 κ°€λŠ₯ + +**둜그 μ˜ˆμ‹œ**: +``` +[info] Attempting BigCommerce sync for user 123 (user@example.com) during login +[info] Successfully synced BigCommerce customer for user 123 during login +``` + +**μž‘λ™ 방식**: +```elixir +def log_in_user(conn, user, params \\ %{}) do + case Recognizer.Accounts.user_prompts(user) do + {:ok, user} -> + # 둜그인 성곡 μ‹œ λ°±κ·ΈλΌμš΄λ“œμ—μ„œ BigCommerce 동기화 μ‹œλ„ + ensure_bigcommerce_user_async(user) + # ... λ‚˜λ¨Έμ§€ 둜그인 처리 + end +end +``` + +### 2. Two-Factor μ„Έμ…˜ μ„€μ • 곡톡 ν•¨μˆ˜ + +**파일**: `lib/recognizer_web/authentication.ex` + +**ν•¨μˆ˜**: `put_two_factor_session/2` + +**λ³€κ²½ λ‚΄μš©**: +- `UserSessionController`와 `UserOAuthController`에 μ€‘λ³΅λ˜μ–΄ 있던 μ½”λ“œλ₯Ό 곡톡 ν•¨μˆ˜λ‘œ μΆ”μΆœ +- μ½”λ“œ 쀑볡 제거둜 μœ μ§€λ³΄μˆ˜μ„± ν–₯상 + +**Before (쀑볡 μ½”λ“œ)**: +```elixir +# UserSessionController +conn +|> put_session(:two_factor_user_id, user.id) +|> put_session(:two_factor_sent, false) +|> put_session(:two_factor_issue_time, System.system_time(:second)) + +# UserOAuthController +conn +|> put_session(:two_factor_user_id, user.id) +|> put_session(:two_factor_sent, false) +``` + +**After (곡톡 ν•¨μˆ˜)**: +```elixir +# 두 컨트둀러 λͺ¨λ‘ +conn |> Authentication.put_two_factor_session(user) +``` + +### 3. BigCommerce 동기화 헬퍼 ν•¨μˆ˜ + +**파일**: `lib/recognizer_web/authentication.ex` + +**ν•¨μˆ˜**: `ensure_bigcommerce_user_async/1` + +**κΈ°λŠ₯**: +- λΉ„λ™κΈ°λ‘œ BigCommerce 동기화 μˆ˜ν–‰ (둜그인 속도에 영ν–₯ μ—†μŒ) +- 이미 λ™κΈ°ν™”λœ μ‚¬μš©μžλŠ” μžλ™μœΌλ‘œ μŠ€ν‚΅ +- μƒμ„Έν•œ λ‘œκΉ…μœΌλ‘œ 디버깅 용이 + +## 문제 ν•΄κ²° + +### apatura.inc@protonmail.com μΌ€μ΄μŠ€ + +**ν•΄κ²° 방법**: μ‚¬μš©μžκ°€ λ‹€μ‹œ λ‘œκ·ΈμΈν•˜λ©΄ μžλ™μœΌλ‘œ λ™κΈ°ν™”λ©λ‹ˆλ‹€. + +1. μ‚¬μš©μžμ—κ²Œ 둜그인 μš”μ²­ +2. 둜그인 μ‹œ μžλ™μœΌλ‘œ λ°±κ·ΈλΌμš΄λ“œμ—μ„œ BigCommerce 동기화 μ‹œλ„ +3. 성곡 μ‹œ 이후 μ£Όλ¬Έ κ°€λŠ₯ + +### κΈ΄κΈ‰ 상황: μ½˜μ†” μ ‘κ·Ό + +ν΄λΌμš°λ“œ ν™˜κ²½μ—μ„œ κΈ΄κΈ‰ν•˜κ²Œ μˆ˜λ™ 동기화가 ν•„μš”ν•œ 경우: + +```bash +# Kubernetes pod 접속 +kubectl exec -it -- iex -S mix + +# IEx μ½˜μ†”μ—μ„œ μ‹€ν–‰ +iex> user = Recognizer.Accounts.get_user_by_email("apatura.inc@protonmail.com") +iex> Recognizer.BigCommerce.get_or_create_customer(user) +``` + +λ˜λŠ” Docker Compose: +```bash +docker-compose exec recognizer iex -S mix +``` + +## 영ν–₯ λ°›λŠ” 파일 + +### μˆ˜μ •λœ 파일 +- `lib/recognizer_web/authentication.ex` - 둜그인 둜직 및 곡톡 ν•¨μˆ˜ μΆ”κ°€ +- `lib/recognizer_web/controllers/accounts/user_session_controller.ex` - 쀑볡 μ½”λ“œ 제거 +- `lib/recognizer_web/controllers/accounts/user_oauth_controller.ex` - 쀑볡 μ½”λ“œ 제거 + +## 둜그인 μ‹œ μžλ™ 동기화가 μΆ©λΆ„ν•œ 이유 + +### βœ… λŒ€λΆ€λΆ„μ˜ μΌ€μ΄μŠ€λ₯Ό μžλ™ ν•΄κ²° +- **계정 생성 μ‹œ μ‹€νŒ¨**: λ‹€μŒ λ‘œκ·ΈμΈμ—μ„œ μžλ™ μž¬μ‹œλ„ +- **μΌμ‹œμ  API 였λ₯˜**: λ‹€μŒ λ‘œκ·ΈμΈμ—μ„œ μžλ™ μž¬μ‹œλ„ +- **λ„€νŠΈμ›Œν¬ 문제**: λ‹€μŒ λ‘œκ·ΈμΈμ—μ„œ μžλ™ μž¬μ‹œλ„ + +### βœ… λ³΄μ•ˆ 이점 +- **API μ—”λ“œν¬μΈνŠΈ μ—†μŒ**: μ•…μš© κ°€λŠ₯μ„± 제둜 +- **Rate limiting λΆˆν•„μš”**: μ‚¬μš©μžκ°€ μžμ—°μŠ€λŸ½κ²Œ μ œν•œλ¨ +- **감사 둜그 λΆˆν•„μš”**: 둜그인 둜그둜 좔적 κ°€λŠ₯ +- **κΆŒν•œ 관리 λΆˆν•„μš”**: μ‚¬μš©μž 본인만 동기화됨 + +### βœ… μ‚¬μš©μž κ²½ν—˜ +- **투λͺ…함**: μ‚¬μš©μžλŠ” 아무것도 ν•  ν•„μš” μ—†μŒ +- **빠름**: λ°±κ·ΈλΌμš΄λ“œ 처리둜 둜그인 속도 영ν–₯ μ—†μŒ +- **μ‹ λ’°μ„±**: μ‹€νŒ¨ν•΄λ„ λ‘œκ·ΈμΈμ€ 성곡 + +### ⚠️ μ œν•œ 사항 +**λ‘œκ·ΈμΈν•˜μ§€ μ•ŠλŠ” μ‚¬μš©μžλŠ” 동기화 μ•ˆ 됨** +- ν•˜μ§€λ§Œ BigCommerce λ™κΈ°ν™”λŠ” μ£Όλ¬Έ μ‹œ ν•„μš” +- μ£Όλ¬Έν•˜λ €λ©΄ 둜그인 ν•„μˆ˜ +- λ”°λΌμ„œ μ‹€μ œλ‘œλŠ” 문제 μ—†μŒ + +### 🚨 κΈ΄κΈ‰ 상황 λŒ€μ‘ +둜그인 전에 동기화가 κΌ­ ν•„μš”ν•œ 경우 (맀우 λ“œλ¬Ύ): +- kubectl/docker exec둜 μ½˜μ†” 접속 +- IExμ—μ„œ μˆ˜λ™ 동기화 +- μ™„μ „ν•œ μ ‘κ·Ό μ œμ–΄ 및 감사 좔적 + +## ν–₯ν›„ κ°œμ„  κ°€λŠ₯ 사항 + +1. **동기화 μž¬μ‹œλ„ 큐**: μ‹€νŒ¨ν•œ 동기화λ₯Ό 주기적으둜 μž¬μ‹œλ„ν•˜λŠ” λ°±κ·ΈλΌμš΄λ“œ μž‘μ—… +2. **동기화 μƒνƒœ ν•„λ“œ**: `users` ν…Œμ΄λΈ”μ— `bc_sync_status` ν•„λ“œ μΆ”κ°€ +3. **λͺ¨λ‹ˆν„°λ§ 및 μ•Œλ¦Ό**: 동기화 μ‹€νŒ¨μœ¨ 좔적 및 μ•Œλ¦Ό μ‹œμŠ€ν…œ +4. **이벀트 μ†Œμ‹±**: 동기화 이벀트λ₯Ό 별도 ν…Œμ΄λΈ”μ— μ €μž₯ν•˜μ—¬ 좔적성 ν–₯상 + +## ν…ŒμŠ€νŠΈ + +컴파일 확인: +```bash +mix compile +``` + +## 참고사항 + +- βœ… 둜그인 μ‹œ μžλ™ λ™κΈ°ν™”λŠ” λ°±κ·ΈλΌμš΄λ“œμ—μ„œ μˆ˜ν–‰λ˜λ―€λ‘œ 둜그인 속도에 영ν–₯ μ—†μŒ +- βœ… 동기화 μ‹€νŒ¨ μ‹œμ—λ„ μ‚¬μš©μžλŠ” μ •μƒμ μœΌλ‘œ 둜그인 κ°€λŠ₯ +- βœ… λͺ¨λ“  동기화 μ‹œλ„λŠ” λ‘œκ·Έμ— κΈ°λ‘λ˜μ–΄ 좔적 κ°€λŠ₯ +- βœ… API μ—”λ“œν¬μΈνŠΈκ°€ μ—†μ–΄ λ³΄μ•ˆ μœ„ν—˜ μ΅œμ†Œν™” +- βœ… μ‚¬μš©μžκ°€ λ‘œκ·ΈμΈν•  λ•Œλ§ˆλ‹€ μžλ™μœΌλ‘œ μž¬μ‹œλ„λ˜μ–΄ κ²°κ΅­ 해결됨 + diff --git a/lib/recognizer_web/controllers/accounts/user_oauth_controller.ex b/lib/recognizer_web/controllers/accounts/user_oauth_controller.ex index 407ab1f7..089aa885 100644 --- a/lib/recognizer_web/controllers/accounts/user_oauth_controller.ex +++ b/lib/recognizer_web/controllers/accounts/user_oauth_controller.ex @@ -35,7 +35,11 @@ defmodule RecognizerWeb.Accounts.UserOAuthController do |> redirect(to: Routes.user_session_path(conn, :new)) {:error, e} -> - {:error, e} + Logger.error("OAuth callback error: #{inspect(e)}") + + conn + |> put_flash(:error, "An error occurred during authentication. Please try again.") + |> redirect(to: Routes.user_session_path(conn, :new)) end end @@ -75,8 +79,14 @@ defmodule RecognizerWeb.Accounts.UserOAuthController do |> provider_params() |> Map.put(:newsletter, true) - with nil <- Accounts.get_user_by_service_guid(provider, uid) do - register_oauth_user(user_params, provider, uid) + case Accounts.get_user_by_service_guid(provider, uid) do + nil -> + # User doesn't exist, create new user + register_oauth_user(user_params, provider, uid) + + result -> + # User already exists, result is {:ok, user} or {:two_factor, user} + result end end @@ -138,6 +148,9 @@ defmodule RecognizerWeb.Accounts.UserOAuthController do def request(conn, %{"provider" => _provider}) do # The Ueberauth plug will handle the actual redirect to the OAuth provider # This action is typically just a passthrough that lets Ueberauth do its work + # If we reach here, something went wrong with the Ueberauth plug conn + |> put_flash(:error, "OAuth request could not be processed.") + |> redirect(to: Routes.user_session_path(conn, :new)) end end From c836810246d8c187c72cd177051c8c7055e0cd37 Mon Sep 17 00:00:00 2001 From: Yongjin Chong Date: Tue, 4 Nov 2025 13:08:30 -0700 Subject: [PATCH 07/15] Plug.conn.not sent error (#181) * Plug.Conn.NotSentError * Plug.Conn.NotSentError * Fix OAuth routing conflict by separating user auth from OAuth provider * Fix OAuth routing conflict by separating user auth from OAuth provider --- config/config.exs | 2 +- lib/recognizer_web/authentication.ex | 12 ++++++++++++ lib/recognizer_web/router.ex | 4 ++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/config/config.exs b/config/config.exs index f14b764e..d2916b59 100644 --- a/config/config.exs +++ b/config/config.exs @@ -69,7 +69,7 @@ config :guardian, Guardian.DB, sweep_interval: 60 config :ueberauth, Ueberauth, - base_path: "/oauth", + base_path: "/auth", providers: [ github: {Ueberauth.Strategy.Github, [default_scope: "user:email", send_redirect_uri: false]}, google: {Ueberauth.Strategy.Google, [default_scope: "email profile"]} diff --git a/lib/recognizer_web/authentication.ex b/lib/recognizer_web/authentication.ex index 5ec98272..c88598fd 100644 --- a/lib/recognizer_web/authentication.ex +++ b/lib/recognizer_web/authentication.ex @@ -90,6 +90,11 @@ defmodule RecognizerWeb.Authentication do get_session(conn, :bc) -> [external: BigCommerce.login_redirect_uri(user)] + # OAuth Provider flow: if user was trying to access /oauth/authorize, return them there + # This takes priority over REDIRECT_URL to preserve OAuth authorization flow + oauth_flow?(conn) -> + [to: get_session(conn, :user_return_to)] + get_session(conn, :user_return_to) -> [to: get_session(conn, :user_return_to)] @@ -101,6 +106,13 @@ defmodule RecognizerWeb.Authentication do end end + defp oauth_flow?(conn) do + case get_session(conn, :user_return_to) do + nil -> false + path -> String.starts_with?(path, "/oauth/authorize") + end + end + @doc """ The URL to redirect the user to once they are logged out. """ diff --git a/lib/recognizer_web/router.ex b/lib/recognizer_web/router.ex index 33d66df7..c9c9bd20 100644 --- a/lib/recognizer_web/router.ex +++ b/lib/recognizer_web/router.ex @@ -89,8 +89,8 @@ defmodule RecognizerWeb.Router do get "/forgot-password/:token", UserResetPasswordController, :edit put "/forgot-password/:token", UserResetPasswordController, :update - get "/oauth/:provider", UserOAuthController, :request, as: :user_oauth - get "/oauth/:provider/callback", UserOAuthController, :callback, as: :user_oauth + get "/auth/:provider", UserOAuthController, :request, as: :user_oauth + get "/auth/:provider/callback", UserOAuthController, :callback, as: :user_oauth get "/two-factor", UserTwoFactorController, :new post "/two-factor", UserTwoFactorController, :create From 1b935269e176a33846b9ee0ee646442054d56d45 Mon Sep 17 00:00:00 2001 From: Yongjin Chong Date: Wed, 5 Nov 2025 20:23:14 -0700 Subject: [PATCH 08/15] Revert OAuth user login routes from /auth back to /oauth (#182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts the route changes from the previous commit to fix Google OAuth redirect_uri_mismatch error. The /auth β†’ /oauth change was unnecessary and caused breaking changes: - Google OAuth Console has redirect_uri registered as /oauth/google/callback - Changing to /auth/google/callback caused redirect_uri_mismatch errors - Route conflict with /oauth/* catch-all was not actually an issue The real fix for Blazer OAuth flow is in authentication.ex's login_redirect function, which now detects OAuth Provider flow and preserves it instead of redirecting to REDIRECT_URL. Changes: - Revert base_path from "/auth" to "/oauth" in Ueberauth config - Revert routes from /auth/:provider to /oauth/:provider Fixes: Google OAuth login redirect_uri_mismatch error --- config/config.exs | 2 +- lib/recognizer_web/router.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/config.exs b/config/config.exs index d2916b59..f14b764e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -69,7 +69,7 @@ config :guardian, Guardian.DB, sweep_interval: 60 config :ueberauth, Ueberauth, - base_path: "/auth", + base_path: "/oauth", providers: [ github: {Ueberauth.Strategy.Github, [default_scope: "user:email", send_redirect_uri: false]}, google: {Ueberauth.Strategy.Google, [default_scope: "email profile"]} diff --git a/lib/recognizer_web/router.ex b/lib/recognizer_web/router.ex index c9c9bd20..33d66df7 100644 --- a/lib/recognizer_web/router.ex +++ b/lib/recognizer_web/router.ex @@ -89,8 +89,8 @@ defmodule RecognizerWeb.Router do get "/forgot-password/:token", UserResetPasswordController, :edit put "/forgot-password/:token", UserResetPasswordController, :update - get "/auth/:provider", UserOAuthController, :request, as: :user_oauth - get "/auth/:provider/callback", UserOAuthController, :callback, as: :user_oauth + get "/oauth/:provider", UserOAuthController, :request, as: :user_oauth + get "/oauth/:provider/callback", UserOAuthController, :callback, as: :user_oauth get "/two-factor", UserTwoFactorController, :new post "/two-factor", UserTwoFactorController, :create From 2d8c8d61e4698f3d3fba86bab62b78bbb3f78503 Mon Sep 17 00:00:00 2001 From: Yongjin Chong Date: Fri, 7 Nov 2025 11:01:59 -0700 Subject: [PATCH 09/15] Routing patch for google (#183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert OAuth user login routes from /auth back to /oauth This reverts the route changes from the previous commit to fix Google OAuth redirect_uri_mismatch error. The /auth β†’ /oauth change was unnecessary and caused breaking changes: - Google OAuth Console has redirect_uri registered as /oauth/google/callback - Changing to /auth/google/callback caused redirect_uri_mismatch errors - Route conflict with /oauth/* catch-all was not actually an issue The real fix for Blazer OAuth flow is in authentication.ex's login_redirect function, which now detects OAuth Provider flow and preserves it instead of redirecting to REDIRECT_URL. Changes: - Revert base_path from "/auth" to "/oauth" in Ueberauth config - Revert routes from /auth/:provider to /oauth/:provider Fixes: Google OAuth login redirect_uri_mismatch error * Fix OAuth routing: restore correct order from before October 29 patch PROBLEM: October 29 security patch (e75abcb) added catch-all route but accidentally reordered OAuth routes. October 30 fix (56e0d80) moved catch-all to end but left routes in wrong order, causing intermittent failures. ROOT CAUSE: Phoenix matches routes top-to-bottom. When /oauth/:provider comes before /oauth/authorize, requests to /oauth/authorize match the pattern route with provider='authorize', routing to UserOAuthController instead of AuthorizeController. SYMPTOMS: - 'OAuth request could not be processed' error - Blazer OAuth fails when user not logged in - Works intermittently when user already authenticated (skips authorize flow) - GitHub/Google login always works (uses /oauth/github, /oauth/google) SOLUTION: Restore original route order from before October 29 while keeping catch-all: 1. /oauth/authorize (specific - OAuth Provider for Blazer etc) 2. /oauth/:provider (pattern - user GitHub/Google login) 3. /oauth/*path (catch-all - security feature from October 29) VERIFICATION: - Matches pre-patch working configuration - All 103 tests pass - Route order confirmed with mix phx.routes This fix is permanent - based on fundamental Phoenix routing rules. --- lib/recognizer_web/router.ex | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/recognizer_web/router.ex b/lib/recognizer_web/router.ex index 33d66df7..f7955e1d 100644 --- a/lib/recognizer_web/router.ex +++ b/lib/recognizer_web/router.ex @@ -65,6 +65,17 @@ defmodule RecognizerWeb.Router do get "/logout", Accounts.UserSessionController, :delete end + # OAuth Provider endpoints (for other services using Recognizer as OAuth provider) + # IMPORTANT: These must come BEFORE /oauth/:provider to prevent route conflicts + scope "/", RecognizerWeb.OauthProvider, as: :oauth do + pipe_through [:browser, :bc, :auth, :user] + + get "/oauth/authorize", AuthorizeController, :new + get "/oauth/authorize/:code", AuthorizeController, :show + post "/oauth/authorize", AuthorizeController, :create + delete "/oauth/authorize", AuthorizeController, :delete + end + scope "/api", RecognizerWeb.Accounts.Api, as: :api do pipe_through [:api, :auth, :user] @@ -128,16 +139,6 @@ defmodule RecognizerWeb.Router do get "/setting/two-factor/resend", UserSettingsController, :resend end - # OAuth Provider endpoints (for other services using Recognizer as OAuth provider) - scope "/", RecognizerWeb.OauthProvider, as: :oauth do - pipe_through [:browser, :bc, :auth, :user] - - get "/oauth/authorize", AuthorizeController, :new - get "/oauth/authorize/:code", AuthorizeController, :show - post "/oauth/authorize", AuthorizeController, :create - delete "/oauth/authorize", AuthorizeController, :delete - end - # OAuth Provider token endpoint and catch-all for attack prevention scope "/", RecognizerWeb.OauthProvider, as: :oauth do pipe_through [:api] From 501fdc64a7d1b22649a1ac2e8b6f1eea67fb122b Mon Sep 17 00:00:00 2001 From: Erin O'Connell Date: Mon, 14 Sep 2026 16:27:29 -0600 Subject: [PATCH 10/15] Bump toolchain to Elixir 1.18.5-otp-27 Bumps CI images, Dockerfile, and .tool-versions from Elixir 1.14.3-otp-25/Erlang 25.3.2.16 to 1.18.5-otp-27/Erlang 27.3.4.17, and fixes what the bump itself surfaced with no dependency changes: - Logger.warn/1 -> Logger.warning/2 (deprecated since Elixir 1.15) at 4 call sites, plus config :logger, level: :warn -> :warning - two unreachable clauses removed (fallback_controller.ex, user_settings_two_factor_controller.ex), surfaced by Elixir 1.18's new type checker; both provably dead, not called anywhere --- .github/workflows/ci.yml | 6 +++--- .tool-versions | 4 ++-- Dockerfile | 2 +- config/test.exs | 2 +- lib/recognizer/bigcommerce/client.ex | 2 +- lib/recognizer/hal.ex | 9 ++++++--- .../accounts/api/user_settings_two_factor_controller.ex | 5 ----- lib/recognizer_web/controllers/fallback_controller.ex | 8 -------- 8 files changed, 14 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0041d1a4..b7428492 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest container: - image: elixir:1.13.3-slim + image: elixir:1.18.4-otp-27-slim services: redis: redis:alpine @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest container: - image: elixir:1.13.3-slim + image: elixir:1.18.4-otp-27-slim steps: - name: Checkout @@ -97,7 +97,7 @@ jobs: runs-on: ubuntu-latest container: - image: elixir:1.13.3-slim + image: elixir:1.18.4-otp-27-slim steps: - name: Checkout diff --git a/.tool-versions b/.tool-versions index bec83f61..96a6121d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -elixir 1.14.3-otp-25 -erlang 25.3.2.16 +elixir 1.18.5-otp-27 +erlang 27.3.4.17 nodejs 14.20.1 diff --git a/Dockerfile b/Dockerfile index b0c969dc..217bd585 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # ----------------------------------------------- # 1) Build Elixir # ----------------------------------------------- -FROM elixir:1.13.3-slim as build-elixir +FROM elixir:1.18.4-otp-27-slim as build-elixir # ARG is available during the build and not in the final container # https://vsupalov.com/docker-arg-vs-env/ diff --git a/config/test.exs b/config/test.exs index d359286e..a50111ff 100644 --- a/config/test.exs +++ b/config/test.exs @@ -20,7 +20,7 @@ config :recognizer, RecognizerWeb.Endpoint, http: [port: 4002], server: false -config :logger, level: :warn +config :logger, level: :warning config :hammer, backend: diff --git a/lib/recognizer/bigcommerce/client.ex b/lib/recognizer/bigcommerce/client.ex index f89165aa..394f19a2 100644 --- a/lib/recognizer/bigcommerce/client.ex +++ b/lib/recognizer/bigcommerce/client.ex @@ -206,7 +206,7 @@ defmodule Recognizer.BigCommerce.Client do {_, retry_value} -> String.to_integer(retry_value) end - Logger.warn("Rate limited, sleeping for ms: #{inspect(retry_ms)}") + Logger.warning("Rate limited, sleeping for ms: #{inspect(retry_ms)}") Process.sleep(retry_ms) end end diff --git a/lib/recognizer/hal.ex b/lib/recognizer/hal.ex index 5f810031..de8807d4 100644 --- a/lib/recognizer/hal.ex +++ b/lib/recognizer/hal.ex @@ -63,7 +63,7 @@ defmodule Recognizer.Hal do {:ok, body} {:error, _reason} when retry_count > 0 -> - Logger.warn("Retrying fetch for #{context_msg}, attempts left: #{retry_count - 1}") + Logger.warning("Retrying fetch for #{context_msg}, attempts left: #{retry_count - 1}") Process.sleep(retry_delay) fetch_data_with_retry(url, context_msg, email_for_log, retry_count - 1, retry_delay * 2) @@ -163,7 +163,10 @@ defmodule Recognizer.Hal do {:ok, %HTTPoison.Response{status_code: status_code, body: _error_body}} when retry_count > 0 and status_code >= 500 -> - Logger.warn("Newsletter update failed with status #{status_code}, retrying. Attempts left: #{retry_count - 1}") + Logger.warning( + "Newsletter update failed with status #{status_code}, retrying. Attempts left: #{retry_count - 1}" + ) + Process.sleep(retry_delay) post_newsletter_with_retry(post_url, payload, email_address, retry_count - 1, retry_delay * 2) @@ -175,7 +178,7 @@ defmodule Recognizer.Hal do {:error, {:http_post_error, status_code}} {:error, %HTTPoison.Error{reason: _reason}} when retry_count > 0 -> - Logger.warn("HTTP error while posting newsletter update, retrying. Attempts left: #{retry_count - 1}") + Logger.warning("HTTP error while posting newsletter update, retrying. Attempts left: #{retry_count - 1}") Process.sleep(retry_delay) post_newsletter_with_retry(post_url, payload, email_address, retry_count - 1, retry_delay * 2) diff --git a/lib/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller.ex b/lib/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller.ex index 0bb90051..e685580b 100644 --- a/lib/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller.ex +++ b/lib/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller.ex @@ -94,11 +94,6 @@ defmodule RecognizerWeb.Accounts.Api.UserSettingsTwoFactorController do conn |> put_status(202) |> render("show.json", settings: settings, user: user) - - {:error, reason} -> - conn - |> put_status(400) - |> json(%{error: reason}) end end end diff --git a/lib/recognizer_web/controllers/fallback_controller.ex b/lib/recognizer_web/controllers/fallback_controller.ex index 8a3d4b3f..8a977f67 100644 --- a/lib/recognizer_web/controllers/fallback_controller.ex +++ b/lib/recognizer_web/controllers/fallback_controller.ex @@ -57,14 +57,6 @@ defmodule RecognizerWeb.FallbackController do |> render("500.html") end - defp respond(conn, :not_found, _template) do - if Application.get_env(:recognizer, :redirect_url) do - redirect(conn, external: Application.get_env(:recognizer, :redirect_url)) - else - redirect(conn, to: Routes.homepage_path(conn, :index)) - end - end - defp respond(conn, type, template) do extension = if json?(conn), do: "json", else: "html" From 16a0913e5f0f0cc32c74521b3402a26923d52c29 Mon Sep 17 00:00:00 2001 From: Erin O'Connell Date: Mon, 14 Sep 2026 16:27:44 -0600 Subject: [PATCH 11/15] Fix critical grpc RCE Pins bottle to system76/bottle@229a577b, which bumps grpc 0.5.0 (< 1.0.0) to ~> 1.0 to fix a critical RCE (GHSA-grp7-v8xh-rj7h / CVE-2026-48853) and moves to amqp ~> 4.0 so rabbit_common resolves cleanly on OTP 27 with no version overrides. grpc ~> 1.0 requires protobuf ~> 0.17, which removed the deprecated Message.new/1 helper every generated struct used to get for free (protobuf's own CHANGELOG: deprecated in v0.15.0, removed in v0.17.0). Recognizer's two call sites switch to struct!/2, matching bottle's own fix for the same break: lib/recognizer/caster.ex and lib/recognizer/notifications/account.ex (the latter used apply(type, :new, [...]) for dynamic dispatch, easy to miss grepping for a literal ".new("). Also bumps spandex ~> 3.2 / spandex_datadog ~> 1.4.0: the old versions capped telemetry at ~> 0.4, incompatible with grpc_core's telemetry ~> 1.0 requirement pulled in by this same bottle bump. --- lib/recognizer/caster.ex | 2 +- lib/recognizer/notifications/account.ex | 2 +- mix.exs | 6 ++-- mix.lock | 38 +++++++++++++------------ 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/lib/recognizer/caster.ex b/lib/recognizer/caster.ex index 90f837c1..e9582675 100644 --- a/lib/recognizer/caster.ex +++ b/lib/recognizer/caster.ex @@ -6,7 +6,7 @@ defmodule Recognizer.Caster do alias Bottle.Account.V1, as: Account def cast(user) do - Account.User.new( + struct!(Account.User, account_type: convert_user_type(user.type), company_name: user.company_name, email: user.email, diff --git a/lib/recognizer/notifications/account.ex b/lib/recognizer/notifications/account.ex index 839efb99..62ce4408 100644 --- a/lib/recognizer/notifications/account.ex +++ b/lib/recognizer/notifications/account.ex @@ -96,7 +96,7 @@ defmodule Recognizer.Notifications.Account do end defp create_message(user, type, args \\ []) do - apply(type, :new, [Keyword.merge([user: user], args)]) + struct!(type, Keyword.merge([user: user], args)) end if Application.compile_env(:ex_aws, :enabled) do diff --git a/mix.exs b/mix.exs index 38efc90e..c12c5292 100644 --- a/mix.exs +++ b/mix.exs @@ -34,7 +34,7 @@ defmodule Recognizer.MixProject do defp deps do [ {:argon2_elixir, "~> 2.0"}, - {:bottle, github: "system76/bottle", ref: "1a49e7bc7d8f7bf556c5780b70e9eb60a06a8ca7"}, + {:bottle, github: "system76/bottle", ref: "229a577bc24ce3e03278d084263fa2a4aab4d367"}, {:cors_plug, "~> 2.0"}, {:cowboy, "~> 2.8", override: true}, {:cowlib, "~> 2.9.1", override: true}, @@ -68,8 +68,8 @@ defmodule Recognizer.MixProject do {:plug_cowboy, "~> 2.4"}, {:pot, "~> 1.0.2"}, {:saxy, "~> 1.1"}, - {:spandex, "~> 3.0.3"}, - {:spandex_datadog, "~> 1.1.0"}, + {:spandex, "~> 3.2"}, + {:spandex_datadog, "~> 1.4.0"}, {:spandex_ecto, "~> 0.6.2"}, {:spandex_phoenix, "~> 1.0.5"}, {:telemetry_metrics, "~> 0.4"}, diff --git a/mix.lock b/mix.lock index 9dd9104c..adbf52b6 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,8 @@ %{ - "amqp": {:hex, :amqp, "3.3.0", "056d9f4bac96c3ab5a904b321e70e78b91ba594766a1fc2f32afd9c016d9f43b", [:mix], [{:amqp_client, "~> 3.9", [hex: :amqp_client, repo: "hexpm", optional: false]}], "hexpm", "8d3ae139d2646c630d674a1b8d68c7f85134f9e8b2a1c3dd5621616994b10a8b"}, - "amqp_client": {:hex, :amqp_client, "3.12.10", "dcc0d5d0037fa2b486c6eb8b52695503765b96f919e38ca864a7b300b829742d", [:make, :rebar3], [{:credentials_obfuscation, "3.4.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:rabbit_common, "3.12.10", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm", "16a23959899a82d9c2534ed1dcf1fa281d3b660fb7f78426b880647f0a53731f"}, + "amqp": {:hex, :amqp, "4.2.1", "c47520b42dfa79e8c6c3647510f6984d9b7981220d1296a5a4192cf561d864af", [:mix], [{:amqp_client, "~> 4.0", [hex: :amqp_client, repo: "hexpm", optional: false]}], "hexpm", "c37ecdd7c03a43816e6160a0e731f340352490ffad7c92af76600291ae660af4"}, + "amqp_client": {:hex, :amqp_client, "4.3.4", "b7ce1678e1e57495d4a170403016faf3808c1301f588325c4024159391797c5a", [:make, :rebar3], [{:credentials_obfuscation, "3.5.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:rabbit_common, "4.3.4", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm", "61418e1ce1097ad87ce51833929c2c1243782f1ace2e49897d8cc5a85ba53753"}, "argon2_elixir": {:hex, :argon2_elixir, "2.4.1", "edb27bdd326bc738f3e4614eddc2f73507be6fedc9533c6bcc6f15bbac9c85cc", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "0e21f52a373739d00bdfd5fe6da2f04eea623cb4f66899f7526dd9db03903d9f"}, - "bottle": {:git, "https://github.com/system76/bottle.git", "1a49e7bc7d8f7bf556c5780b70e9eb60a06a8ca7", [ref: "1a49e7bc7d8f7bf556c5780b70e9eb60a06a8ca7"]}, + "bottle": {:git, "https://github.com/system76/bottle.git", "229a577bc24ce3e03278d084263fa2a4aab4d367", [ref: "229a577bc24ce3e03278d084263fa2a4aab4d367"]}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, @@ -12,7 +12,7 @@ "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, - "credentials_obfuscation": {:hex, :credentials_obfuscation, "3.4.0", "34e18b126b3aefd6e8143776fbe1ceceea6792307c99ac5ee8687911f048cfd7", [:rebar3], [], "hexpm", "738ace0ed5545d2710d3f7383906fc6f6b582d019036e5269c4dbd85dbced566"}, + "credentials_obfuscation": {:hex, :credentials_obfuscation, "3.5.0", "61e282adfb4439486b3994faaec69543c7ee6cc7e70c6340e8853fd9deaf8219", [:rebar3], [], "hexpm", "843adbe3246861ce0f1a0fa3222f384834eb31defd8d6b9cba7afd2977c957bc"}, "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, @@ -30,8 +30,10 @@ "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "gettext": {:hex, :gettext, "0.23.1", "821e619a240e6000db2fc16a574ef68b3bd7fe0167ccc264a81563cc93e67a31", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "19d744a36b809d810d610b57c27b934425859d158ebd56561bc41f7eeb8795db"}, "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm", "99cb4128cffcb3227581e5d4d803d5413fa643f4eb96523f77d9e6937d994ceb"}, - "google_protos": {:hex, :google_protos, "0.3.0", "15faf44dce678ac028c289668ff56548806e313e4959a3aaf4f6e1ebe8db83f4", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1f6b7fb20371f72f418b98e5e48dae3e022a9a6de1858d4b254ac5a5d0b4035f"}, - "grpc": {:hex, :grpc, "0.5.0", "a44cb306625a52fa31a2189ce91b40d24e82569568f0cc214c1e1e0faf54f58a", [:mix], [{:cowboy, "~> 2.9", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}, {:gun, "~> 2.0.1", [hex: :grpc_gun, repo: "hexpm", optional: false]}], "hexpm", "17b98593fdb1a65be7b2722821266627b3f2fba29bbbd7d0945389427c0d0d5f"}, + "google_protos": {:hex, :google_protos, "0.4.0", "93e1be2c1a07517ffed761f69047776caf35e4acd385aac4f5ce4fedd07f3660", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "4c54983d78761a3643e2198adf0f5d40a5a8b08162f3fc91c50faa257f3fa19f"}, + "googleapis": {:hex, :googleapis, "0.1.0", "13770f3f75f5b863fb9acf41633c7bc71bad788f3f553b66481a096d083ee20e", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1989a7244fd17d3eb5f3de311a022b656c3736b39740db46506157c4604bd212"}, + "grpc": {:hex, :grpc, "1.0.5", "71082f1a2d0ff6d2a4198efaf514c33e71485167d1e278022bc0641cd788cea2", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:grpc_core, "~> 1.0.5", [hex: :grpc_core, repo: "hexpm", optional: false]}, {:gun, "~> 2.4.0", [hex: :gun, repo: "hexpm", optional: true]}, {:mint, "~> 1.9", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "5e892e542a2e0f05ed17a4ad3814483e3980eaa93906f993dac9e08369b178ea"}, + "grpc_core": {:hex, :grpc_core, "1.0.5", "add39a2ca38604f73c8b2feed30c0fd2d2291daedac8060ee1a75b5658f0a69d", [:mix], [{:googleapis, "~> 0.1.0", [hex: :googleapis, repo: "hexpm", optional: false]}, {:jason, ">= 0.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.17", [hex: :protobuf, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a18ae7380cfebc20271aad36393e8aa041e1faa926fa25f40328e4350f83d8f0"}, "guardian": {:hex, :guardian, "2.3.2", "78003504b987f2b189d76ccf9496ceaa6a454bb2763627702233f31eb7212881", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b189ff38cd46a22a8a824866a6867ca8722942347f13c33f7d23126af8821b52"}, "guardian_db": {:hex, :guardian_db, "2.1.0", "ec95a9d99cdd1e550555d09a7bb4a340d8887aad0697f594590c2fd74be02426", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f8e7d543ac92c395f3a7fd5acbe6829faeade57d688f7562e2f0fca8f94a0d70"}, "gun": {:hex, :grpc_gun, "2.0.1", "221b792df3a93e8fead96f697cbaf920120deacced85c6cd3329d2e67f0871f8", [:rebar3], [{:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "795a65eb9d0ba16697e6b0e1886009ce024799e43bb42753f0c59b029f592831"}, @@ -51,11 +53,11 @@ "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, - "msgpax": {:hex, :msgpax, "2.2.4", "7b3790ef684089076b63c0f08c2f4b079c6311daeb006b69e4ed2bf67518291e", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b351b6d992d79624a8430a99d21a41b36b1b90edf84326a294e9f4a2de11f089"}, + "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, "myxql": {:hex, :myxql, "0.6.3", "3d77683a09f1227abb8b73d66b275262235c5cae68182f0cfa5897d72a03700e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "af9eb517ddaced5c5c28e8749015493757fd4413f2cfccea449c466d405d9f51"}, "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, "oauth2": {:hex, :oauth2, "2.0.1", "70729503e05378697b958919bb2d65b002ba6b28c8112328063648a9348aaa3f", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "c64e20d4d105bcdbcbe03170fb530d0eddc3a3e6b135a87528a22c8aecf74c52"}, - "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"}, + "optimal": {:hex, :optimal, "0.3.7", "d614c07dddef168c6c31fe45c12f2cd1b1a0ae827f639c7eb8e4c374c58f1854", [:mix], [], "hexpm", "d649ecd5208d9053b406f1ad85f2830698cf99734d75a4ba82a0027bb8538ce8"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, @@ -71,21 +73,21 @@ "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, - "protobuf": {:hex, :protobuf, "0.11.0", "58d5531abadea3f71135e97bd214da53b21adcdb5b1420aee63f4be8173ec927", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "30ad9a867a5c5a0616cac9765c4d2c2b7b0030fa81ea6d0c14c2eb5affb6ac52"}, - "rabbit_common": {:hex, :rabbit_common, "3.12.10", "7fc633ee206ae48783d8a5302dfc8fe1e086a5d7de494785ed206f586ad64b34", [:make, :rebar3], [{:credentials_obfuscation, "3.4.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:recon, "2.5.3", [hex: :recon, repo: "hexpm", optional: false]}, {:thoas, "1.0.0", [hex: :thoas, repo: "hexpm", optional: false]}], "hexpm", "908a8b1bd059f5baefe225fe9d3e2545d35a28db8f6a14d60372556ca7afe641"}, - "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, - "recon": {:hex, :recon, "2.5.3", "739107b9050ea683c30e96de050bc59248fd27ec147696f79a8797ff9fa17153", [:mix, :rebar3], [], "hexpm", "6c6683f46fd4a1dfd98404b9f78dcabc7fcd8826613a89dcb984727a8c3099d7"}, + "protobuf": {:hex, :protobuf, "0.17.0", "39e24e43c9648e148feba16ed51100b5b2028ea900b55460377b0476f6e10613", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ca6c91f6f63e2c147b47f03eefd10b80538aa6fc55ff4b12b795efb786b0152f"}, + "rabbit_common": {:hex, :rabbit_common, "4.3.4", "2911b2b563aa6a477c55bb1f4cbf83fcb7accc5412722b3bb5f3ae4e5e33f877", [:make, :rebar3], [{:credentials_obfuscation, "3.5.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:ranch, "2.2.0", [hex: :ranch, repo: "hexpm", optional: false]}, {:recon, "2.5.6", [hex: :recon, repo: "hexpm", optional: false]}, {:thoas, "1.2.1", [hex: :thoas, repo: "hexpm", optional: false]}], "hexpm", "e5fefde66701a0d6ce7cc464133c6975b0b2661b47561d7c622cce6b7420413c"}, + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, + "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "redix": {:hex, :redix, "1.3.0", "f4121163ff9d73bf72157539ff23b13e38422284520bb58c05e014b19d6f0577", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "60d483d320c77329c8cbd3df73007e51b23f3fae75b7693bc31120d83ab26131"}, "saxy": {:hex, :saxy, "1.5.0", "0141127f2d042856f135fb2d94e0beecda7a2306f47546dbc6411fc5b07e28bf", [:mix], [], "hexpm", "ea7bb6328fbd1f2aceffa3ec6090bfb18c85aadf0f8e5030905e84235861cf89"}, - "spandex": {:hex, :spandex, "3.0.3", "91aa318f3de696bb4d931adf65f7ebdbe5df25cccce1fe8fd376a44c46bcf69b", [:mix], [{:decorator, "~> 1.2", [hex: :decorator, repo: "hexpm", optional: true]}, {:optimal, "~> 0.3.3", [hex: :optimal, repo: "hexpm", optional: false]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e3e6c319d0ab478ddc9a39102a727a410c962b4d51c0932c72279b86d3b17044"}, - "spandex_datadog": {:hex, :spandex_datadog, "1.1.0", "8c84e2f6c4067edc2e920dd79242f7bb0d6403652a7e9bc42109007f76b9be25", [:mix], [{:msgpax, "~> 2.2.1", [hex: :msgpax, repo: "hexpm", optional: false]}, {:spandex, "~> 3.0", [hex: :spandex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4c20d3e601cad869705d9789f17a9242f245ce0bf2579fc835e96a6834663e2"}, + "spandex": {:hex, :spandex, "3.2.0", "f8cd40146ea988c87f3c14054150c9a47ba17e53cd4515c00e1f93c29c45404d", [:mix], [{:decorator, "~> 1.2", [hex: :decorator, repo: "hexpm", optional: true]}, {:optimal, "~> 0.3.3", [hex: :optimal, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d0a7d5aef4c5af9cf5467f2003e8a5d8d2bdae3823a6cc95d776b9a2251d4d03"}, + "spandex_datadog": {:hex, :spandex_datadog, "1.4.0", "0594b9655b0af00ab9137122616bc0208b68ceec01e9916ab13d6fbb33dcce35", [:mix], [{:msgpax, "~> 2.2.1 or ~> 2.3", [hex: :msgpax, repo: "hexpm", optional: false]}, {:spandex, "~> 3.2", [hex: :spandex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "360f8e1b4db238c1749c4872b1697b096429927fa42b8858d0bb782067380123"}, "spandex_ecto": {:hex, :spandex_ecto, "0.6.2", "845e0e0a115e84c218015e8a13cca7adb38e8b0a1b45010a51451a8c9961c551", [:mix], [{:spandex, "~> 2.2 or ~> 3.0", [hex: :spandex, repo: "hexpm", optional: false]}], "hexpm", "ddeb5c279ca850a38eee6decc8f91b3f4929c76f141b3329293793be54d0c1c7"}, "spandex_phoenix": {:hex, :spandex_phoenix, "1.0.6", "b2caf99cd37cf5c501c89de6099b07a8efab31747dbd63ed2fff802bb02c6937", [:mix], [{:optimal, "~> 0.3", [hex: :optimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3", [hex: :plug, repo: "hexpm", optional: false]}, {:spandex, "~> 2.2 or ~> 3.0", [hex: :spandex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "e286d4bfe6917ecddf56b47553322b55bdc328326b7d86a6c35b4835679e9784"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, - "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, - "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"}, - "thoas": {:hex, :thoas, "1.0.0", "567c03902920827a18a89f05b79a37b5bf93553154b883e0131801600cf02ce0", [:rebar3], [], "hexpm", "fc763185b932ecb32a554fb735ee03c3b6b1b31366077a2427d2a97f3bd26735"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, + "telemetry_poller": {:hex, :telemetry_poller, "0.4.0", "da64dea54b77604023e8d15dc61a5df8968f4c9e013eba561bfb2bc614b15432", [:rebar3], [], "hexpm", "f3374de85219675fceedd13386a39768c6f5e4b1a439a502da8c7dc142a43367"}, + "thoas": {:hex, :thoas, "1.2.1", "19a25f31177a17e74004d4840f66d791d4298c5738790fa2cc73731eb911f195", [:rebar3], [], "hexpm", "e38697edffd6e91bd12cea41b155115282630075c2a727e7a6b2947f5408b86a"}, "ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"}, "ueberauth_github": {:hex, :ueberauth_github, "0.8.3", "1c478629b4c1dae446c68834b69194ad5cead3b6c67c913db6fdf64f37f0328f", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "ae0ab2879c32cfa51d7287a48219b262bfdab0b7ec6629f24160564247493cc6"}, "ueberauth_google": {:hex, :ueberauth_google, "0.12.1", "90cf49743588193334f7a00da252f92d90bfd178d766c0e4291361681fafec7d", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.10.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "7f7deacd679b2b66e3bffb68ecc77aa1b5396a0cbac2941815f253128e458c38"}, From 2ef1104a63caea718481bc8dc39b56d4df3f0029 Mon Sep 17 00:00:00 2001 From: Erin O'Connell Date: Mon, 14 Sep 2026 16:27:56 -0600 Subject: [PATCH 12/15] Swap logger_json to the maintained Hex package Nebo15/logger_json@8e4290a was pinned to a 2021 git fork (reasonable at the time -- Hex hadn't seen a release since 2019) but the project is actively maintained again through 7.0.4 as of Jul 2025. Moves to {:logger_json, "~> 7.0"}. 7.0's API was redesigned around Elixir's :default_handler model: - config/config.exs: drops the old `config :logger_json, :backend, ...` key, which no longer exists - config/prod.exs: `backends: [LoggerJSON]` -> `default_handler: [formatter: {LoggerJSON.Formatters.Datadog, metadata: :all}]` -- must use the {Module, opts} tuple form, not `.new(...)`, since compile-time config evaluates before deps are compiled and calling `.new(...)` directly deadlocks a from-scratch `_build/prod` boot - endpoint.ex: removes `plug LoggerJSON.Plug, ...` (no longer a Plug module) - telemetry.ex: attaches LoggerJSON.Plug.telemetry_logging_handler/4 to the phoenix/endpoint/stop event, replacing what the removed endpoint plug used to do, mirroring the existing Ecto handler pattern --- config/config.exs | 4 ---- config/prod.exs | 4 ++-- lib/recognizer_web/endpoint.ex | 3 --- lib/recognizer_web/telemetry.ex | 8 ++++++++ mix.exs | 2 +- mix.lock | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/config/config.exs b/config/config.exs index f14b764e..6b8ceb31 100644 --- a/config/config.exs +++ b/config/config.exs @@ -29,10 +29,6 @@ config :logger, :console, config :grpc, start_server: true -config :logger_json, :backend, - formatter: LoggerJSON.Formatters.DatadogLogger, - metadata: :all - config :phoenix, :json_library, Jason config :recognizer, :message_queues, [] diff --git a/config/prod.exs b/config/prod.exs index b5fec7a3..ecafe42b 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -8,8 +8,8 @@ config :recognizer, RecognizerWeb.Endpoint, server: true config :logger, - backends: [LoggerJSON], - level: :info + level: :info, + default_handler: [formatter: {LoggerJSON.Formatters.Datadog, metadata: :all}] config :recognizer, Recognizer.Repo, log: false diff --git a/lib/recognizer_web/endpoint.ex b/lib/recognizer_web/endpoint.ex index e44b2c9d..7281a8b1 100644 --- a/lib/recognizer_web/endpoint.ex +++ b/lib/recognizer_web/endpoint.ex @@ -30,9 +30,6 @@ defmodule RecognizerWeb.Endpoint do plug Bottle.RequestIdPlug plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] - plug LoggerJSON.Plug, - metadata_formatter: LoggerJSON.Plug.MetadataFormatters.DatadogLogger - plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], diff --git a/lib/recognizer_web/telemetry.ex b/lib/recognizer_web/telemetry.ex index b6d296b9..b15f3371 100644 --- a/lib/recognizer_web/telemetry.ex +++ b/lib/recognizer_web/telemetry.ex @@ -27,6 +27,14 @@ defmodule RecognizerWeb.Telemetry do :debug ) + :ok = + :telemetry.attach( + "logger-json-requests", + [:phoenix, :endpoint, :stop], + &LoggerJSON.Plug.telemetry_logging_handler/4, + :info + ) + :ok = :telemetry.attach( "spandex-query-tracer-repo_name", diff --git a/mix.exs b/mix.exs index c12c5292..c1660670 100644 --- a/mix.exs +++ b/mix.exs @@ -56,7 +56,7 @@ defmodule Recognizer.MixProject do {:httpoison, "~> 1.8.2"}, {:jason, "~> 1.0"}, {:joken, "~> 2.6.0"}, - {:logger_json, github: "Nebo15/logger_json", ref: "8e4290a"}, + {:logger_json, "~> 7.0"}, {:myxql, ">= 0.0.0"}, {:redix, ">= 0.0.0"}, {:phoenix_ecto, "~> 4.1"}, diff --git a/mix.lock b/mix.lock index adbf52b6..03aecd0d 100644 --- a/mix.lock +++ b/mix.lock @@ -48,7 +48,7 @@ "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, "jsx": {:hex, :jsx, "3.1.0", "d12516baa0bb23a59bb35dccaf02a1bd08243fcbb9efe24f2d9d056ccff71268", [:rebar3], [], "hexpm", "0c5cc8fdc11b53cc25cf65ac6705ad39e54ecc56d1c22e4adb8f5a53fb9427f3"}, "lager": {:hex, :lager, "3.9.2", "4cab289120eb24964e3886bd22323cb5fefe4510c076992a23ad18cf85413d8c", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm", "7f904d9e87a8cb7e66156ed31768d1c8e26eba1d54f4bc85b1aa4ac1f6340c28"}, - "logger_json": {:git, "https://github.com/Nebo15/logger_json.git", "8e4290a7377a3624b3eaab9db1889d7e9c537bb7", [ref: "8e4290a"]}, + "logger_json": {:hex, :logger_json, "7.0.4", "e315f2b9a755504658a745f3eab90d88d2cd7ac2ecfd08c8da94d8893965ab5c", [:mix], [{:decimal, ">= 0.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d1369f8094e372db45d50672c3b91e8888bcd695fdc444a37a0734e96717c45c"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, From 0595f15a641bb52611e8856830bb50495e6df91c Mon Sep 17 00:00:00 2001 From: Erin O'Connell Date: Mon, 14 Sep 2026 16:28:07 -0600 Subject: [PATCH 13/15] Bump Phoenix 1.7 -> 1.8 The 1.7 branch is EOL (last release Mar 2025); all security fixes since are 1.8-only. Bumps phoenix ~> 1.7.1 -> ~> 1.8 (1.7.12 -> 1.8.14). 1.8 removed the `namespace:` controller option recognizer's shared `RecognizerWeb.controller/0` macro used, and now requires an explicit `:formats` option. Fixed by reading Phoenix's actual source (__plugs__/2 in phoenix/lib/phoenix/controller.ex) rather than the compiler warning's suggested snippet, which would raise "no previous layout set" at the first HTML render: use Phoenix.Controller, formats: [html: "View", json: "View"] plug :put_new_layout, {RecognizerWeb.LayoutView, :app} This matches exactly what the old `namespace:` fallback did internally. Also adds `listeners: [Phoenix.CodeReloader]` to mix.exs -- a new required knob in 1.8 for the dev code-reloader (was printing a warning on every dev request otherwise). --- lib/recognizer_web.ex | 4 +++- mix.exs | 5 +++-- mix.lock | 12 ++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/recognizer_web.ex b/lib/recognizer_web.ex index 678e80e7..ae94712b 100644 --- a/lib/recognizer_web.ex +++ b/lib/recognizer_web.ex @@ -19,7 +19,9 @@ defmodule RecognizerWeb do def controller do quote do - use Phoenix.Controller, namespace: RecognizerWeb + use Phoenix.Controller, formats: [html: "View", json: "View"] + + plug :put_new_layout, {RecognizerWeb.LayoutView, :app} import Plug.Conn import RecognizerWeb.Gettext diff --git a/mix.exs b/mix.exs index c1660670..8150c392 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,8 @@ defmodule Recognizer.MixProject do start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps(), - releases: releases() + releases: releases(), + listeners: [Phoenix.CodeReloader] ] end @@ -64,7 +65,7 @@ defmodule Recognizer.MixProject do {:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_html_helpers, "~> 1.0.1"}, {:phoenix_view, "~> 2.0.3"}, - {:phoenix, "~> 1.7.1"}, + {:phoenix, "~> 1.8"}, {:plug_cowboy, "~> 2.4"}, {:pot, "~> 1.0.2"}, {:saxy, "~> 1.1"}, diff --git a/mix.lock b/mix.lock index 03aecd0d..c20cac2c 100644 --- a/mix.lock +++ b/mix.lock @@ -59,18 +59,18 @@ "oauth2": {:hex, :oauth2, "2.0.1", "70729503e05378697b958919bb2d65b002ba6b28c8112328063648a9348aaa3f", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "c64e20d4d105bcdbcbe03170fb530d0eddc3a3e6b135a87528a22c8aecf74c52"}, "optimal": {:hex, :optimal, "0.3.7", "d614c07dddef168c6c31fe45c12f2cd1b1a0ae827f639c7eb8e4c374c58f1854", [:mix], [], "hexpm", "d649ecd5208d9053b406f1ad85f2830698cf99734d75a4ba82a0027bb8538ce8"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, + "phoenix": {:hex, :phoenix, "1.8.14", "9279cbbcd755ac8d5f42d206915e7df13b9ab49096b98114f1c99ff908e5b42e", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 2.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "2782ff375824b2b5e41561fbae4764ee7b875af6898483bca49f24a9d1e37816"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, - "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.3.0", "03916bfbc31a5121945b3cfffe5aec647a5c97fe1dc172a319b94428562359c9", [:mix], [], "hexpm", "eec7be6e9cf02e2551d389b558402d6c637cd3973796326e7ba4bb03c6b2e91d"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "phx_gen_auth": {:hex, :phx_gen_auth, "0.7.0", "2e10e9527b6b71abbfbb4601c7dc4aa4fb9f2db6f9a6be457c468b7f2b0f6319", [:mix], [{:phoenix, "~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b9dc3e3b866e67c5db8f00f4a2adb28fc8636e794f78600e35aba0e55bdac209"}, "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, - "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, + "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, "protobuf": {:hex, :protobuf, "0.17.0", "39e24e43c9648e148feba16ed51100b5b2028ea900b55460377b0476f6e10613", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ca6c91f6f63e2c147b47f03eefd10b80538aa6fc55ff4b12b795efb786b0152f"}, @@ -93,5 +93,5 @@ "ueberauth_google": {:hex, :ueberauth_google, "0.12.1", "90cf49743588193334f7a00da252f92d90bfd178d766c0e4291361681fafec7d", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.10.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "7f7deacd679b2b66e3bffb68ecc77aa1b5396a0cbac2941815f253128e458c38"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, + "websock_adapter": {:hex, :websock_adapter, "0.6.0", "73db5ab8aaefd1a876a97ce3e6afc96562625de69ef17a4e04426e034849d0b8", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "50021a85bce8f203b086705d9e0c5415e2c7eb05d319111b0428fe71f9934617"}, } From 158a5201707517722118a7e81e9b329c882df8de Mon Sep 17 00:00:00 2001 From: Erin O'Connell Date: Mon, 14 Sep 2026 16:28:27 -0600 Subject: [PATCH 14/15] Fix remaining pre-existing CVEs Fixes CVEs found in recognizer's own deps while validating the OTP 27 bump above, all pre-existing on master (confirmed via a pre-change mix.lock diff and a stash round-trip boot test): - guardian ~> 2.0 -> ~> 2.5 (2.3.2 -> 2.5.0, fixes 3 CVEs incl. forged-token revocation) - httpoison ~> 1.8.2 -> ~> 3.0 (1.8.2 -> 3.0.0; public HTTPoison.get/post/%Response{}/%Error{} API unchanged across majors, verified against call sites in hal.ex/client.ex) - joken ~> 2.6.0 -> ~> 2.7, pulling in jose ~> 1.11.12 (fixes the jose DoS CVE; jose isn't a direct dep) - plug_cowboy ~> 2.4 -> ~> 2.9 - cowboy/cowlib overrides ~> 2.8/~> 2.9.1 -> ~> 2.19/~> 2.20 (cowlib 2.9.1 had 7 CVEs; 2.20.0, the latest upstream release, still carries 3 unpatched ones -- nothing more to do via version bump) - hackney override ~> 4.0, needed because ex_aws_sqs's optional hackney ~> 1.9 dep conflicts with httpoison 3.0's hackney ~> 4.0 requirement; recognizer configures http_client: HTTPoison for ExAws in every env, so ex_aws_sqs's hackney-based adapter path is dead weight, not a real runtime pairing - decimal (transitive via ecto) was capped at 2.4.1 by myxql being stuck at 0.6.3, which capped ecto_sql below the 3.12+ that requires myxql ~> 0.8; unlocked myxql/ecto/ecto_sql/decimal together to let the whole chain move (myxql 0.6.3->0.9.0, ecto_sql 3.11.3->3.12.1, decimal 2.4.1->3.1.1) --- mix.exs | 15 +++++++++------ mix.lock | 43 +++++++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/mix.exs b/mix.exs index 8150c392..761e03f2 100644 --- a/mix.exs +++ b/mix.exs @@ -37,8 +37,8 @@ defmodule Recognizer.MixProject do {:argon2_elixir, "~> 2.0"}, {:bottle, github: "system76/bottle", ref: "229a577bc24ce3e03278d084263fa2a4aab4d367"}, {:cors_plug, "~> 2.0"}, - {:cowboy, "~> 2.8", override: true}, - {:cowlib, "~> 2.9.1", override: true}, + {:cowboy, "~> 2.19", override: true}, + {:cowlib, "~> 2.20", override: true}, {:credo, "~> 1.5", only: [:dev, :test], runtime: false}, {:decorator, "~> 1.2"}, {:ecto_enum, "~> 1.4"}, @@ -49,14 +49,17 @@ defmodule Recognizer.MixProject do {:ex_aws, "~> 2.0"}, {:ex_oauth2_provider, "~> 0.5.6"}, {:gettext, "~> 0.18"}, - {:guardian, "~> 2.0"}, + {:guardian, "~> 2.5"}, {:guardian_db, "~> 2.1"}, + # ex_aws_sqs's hackney dep is optional and unused (we configure HTTPoison as the ex_aws + # http_client); override so its stale ~> 1.9 pin doesn't block httpoison's real ~> 4.0 need + {:hackney, "~> 4.0", override: true}, {:hammer, "~> 6.0"}, {:hammer_backend_redis, "~> 6.1"}, {:hammer_plug, "~> 3.0"}, - {:httpoison, "~> 1.8.2"}, + {:httpoison, "~> 3.0"}, {:jason, "~> 1.0"}, - {:joken, "~> 2.6.0"}, + {:joken, "~> 2.7"}, {:logger_json, "~> 7.0"}, {:myxql, ">= 0.0.0"}, {:redix, ">= 0.0.0"}, @@ -66,7 +69,7 @@ defmodule Recognizer.MixProject do {:phoenix_html_helpers, "~> 1.0.1"}, {:phoenix_view, "~> 2.0.3"}, {:phoenix, "~> 1.8"}, - {:plug_cowboy, "~> 2.4"}, + {:plug_cowboy, "~> 2.9"}, {:pot, "~> 1.0.2"}, {:saxy, "~> 1.1"}, {:spandex, "~> 3.2"}, diff --git a/mix.lock b/mix.lock index c20cac2c..b86e0755 100644 --- a/mix.lock +++ b/mix.lock @@ -5,21 +5,21 @@ "bottle": {:git, "https://github.com/system76/bottle.git", "229a577bc24ce3e03278d084263fa2a4aab4d367", [ref: "229a577bc24ce3e03278d084263fa2a4aab4d367"]}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, - "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "certifi": {:hex, :certifi, "2.17.0", "835748414307e15e05b17d0e518190228ce648b08d569a5cc93a85a40f3e5c9b", [:rebar3], [], "hexpm", "8122798a17f0293c80daada25d0f81c7f4d708c73fef782c7c9b1950e26e4d21"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cors_plug": {:hex, :cors_plug, "2.0.3", "316f806d10316e6d10f09473f19052d20ba0a0ce2a1d910ddf57d663dac402ae", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ee4ae1418e6ce117fc42c2ba3e6cbdca4e95ecd2fe59a05ec6884ca16d469aea"}, - "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, - "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, - "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, + "cowboy": {:hex, :cowboy, "2.19.0", "78b9d92d25a23e56d7341040b67946c04067cd77de966e2372865f673a8e6f59", [:make, :rebar3], [{:cowlib, ">= 2.20.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "986dae81f99fcb78ef2d8efc21d738cee189410840eeaf32eca84ada81dcf6d4"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.20.0", "bb525377ba634cd6d68bac7bff5d98571f738806139d335daca827717c5dc172", [:make, :rebar3], [], "hexpm", "7d41a0dd2c093041ff3779ac5fe8a1585a68ec7cb2dd1de0536bdd2452fd7ba1"}, "credentials_obfuscation": {:hex, :credentials_obfuscation, "3.5.0", "61e282adfb4439486b3994faaec69543c7ee6cc7e70c6340e8853fd9deaf8219", [:rebar3], [], "hexpm", "843adbe3246861ce0f1a0fa3222f384834eb31defd8d6b9cba7afd2977c957bc"}, "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, - "ecto": {:hex, :ecto, "3.11.0", "ff8614b4e70a774f9d39af809c426def80852048440e8785d93a6e91f48fec00", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7769dad267ef967310d6e988e92d772659b11b09a0c015f101ce0fff81ce1f81"}, + "ecto": {:hex, :ecto, "3.14.2", "99db28a864293a789c970651de711e3cae184291e0e7ea1166c54055ac41c1f3", [:mix], [{:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25d60b8c816a07d19d85b80bdf60978bd8b102209dda198d768cd7c6745339a6"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, - "ecto_sql": {:hex, :ecto_sql, "3.11.0", "c787b24b224942b69c9ff7ab9107f258ecdc68326be04815c6cce2941b6fad1c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "77aa3677169f55c2714dda7352d563002d180eb33c0dc29cd36d39c0a1a971f5"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"}, "eqrcode": {:hex, :eqrcode, "0.1.10", "6294fece9d68ad64eef1c3c92cf111cfd6469f4fbf230a2d4cc905a682178f3f", [:mix], [], "hexpm", "da30e373c36a0fd37ab6f58664b16029919896d6c45a68a95cc4d713e81076f1"}, "ex_aws": {:hex, :ex_aws, "2.5.0", "1785e69350b16514c1049330537c7da10039b1a53e1d253bbd703b135174aec3", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "971b86e5495fc0ae1c318e35e23f389e74cf322f2c02d34037c6fc6d405006f1"}, @@ -34,31 +34,32 @@ "googleapis": {:hex, :googleapis, "0.1.0", "13770f3f75f5b863fb9acf41633c7bc71bad788f3f553b66481a096d083ee20e", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1989a7244fd17d3eb5f3de311a022b656c3736b39740db46506157c4604bd212"}, "grpc": {:hex, :grpc, "1.0.5", "71082f1a2d0ff6d2a4198efaf514c33e71485167d1e278022bc0641cd788cea2", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:grpc_core, "~> 1.0.5", [hex: :grpc_core, repo: "hexpm", optional: false]}, {:gun, "~> 2.4.0", [hex: :gun, repo: "hexpm", optional: true]}, {:mint, "~> 1.9", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "5e892e542a2e0f05ed17a4ad3814483e3980eaa93906f993dac9e08369b178ea"}, "grpc_core": {:hex, :grpc_core, "1.0.5", "add39a2ca38604f73c8b2feed30c0fd2d2291daedac8060ee1a75b5658f0a69d", [:mix], [{:googleapis, "~> 0.1.0", [hex: :googleapis, repo: "hexpm", optional: false]}, {:jason, ">= 0.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.17", [hex: :protobuf, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a18ae7380cfebc20271aad36393e8aa041e1faa926fa25f40328e4350f83d8f0"}, - "guardian": {:hex, :guardian, "2.3.2", "78003504b987f2b189d76ccf9496ceaa6a454bb2763627702233f31eb7212881", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b189ff38cd46a22a8a824866a6867ca8722942347f13c33f7d23126af8821b52"}, + "guardian": {:hex, :guardian, "2.5.0", "dfe9533d734e1ca0341eaf1f7c951513b1c19006db69beaf2bae16cf63845ac2", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bc75cc9374825194060b0cd3230ce4440df23d8509d1f17472b210d3ee525790"}, "guardian_db": {:hex, :guardian_db, "2.1.0", "ec95a9d99cdd1e550555d09a7bb4a340d8887aad0697f594590c2fd74be02426", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f8e7d543ac92c395f3a7fd5acbe6829faeade57d688f7562e2f0fca8f94a0d70"}, "gun": {:hex, :grpc_gun, "2.0.1", "221b792df3a93e8fead96f697cbaf920120deacced85c6cd3329d2e67f0871f8", [:rebar3], [{:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "795a65eb9d0ba16697e6b0e1886009ce024799e43bb42753f0c59b029f592831"}, - "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "h2": {:hex, :h2, "0.12.0", "f393539ee2728f8118fb2024b6d5f3e2c45e40ceb31b18b4e9bf5e50d028f80f", [:rebar3], [], "hexpm", "beaafc93c54cdc5d623247334d3970cdf4bc66b6b8b296b74ba1d7c7513c3dfc"}, + "hackney": {:hex, :hackney, "4.7.4", "8fe2ddaa3ca27de99d68e682d72b66d07d2331da680f77c8000580a0122c69e6", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.12.0", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.5", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.5", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "d07d7e1358353ab6cc75132f058c155287f3e013d43f709fbb79d79eeab98195"}, "hammer": {:hex, :hammer, "6.1.0", "f263e3c3e9946bd410ea0336b2abe0cb6260af4afb3a221e1027540706e76c55", [:make, :mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b47e415a562a6d072392deabcd58090d8a41182cf9044cdd6b0d0faaaf68ba57"}, "hammer_backend_redis": {:hex, :hammer_backend_redis, "6.1.2", "eb296bb4924928e24135308b2afc189201fd09411c870c6bbadea444a49b2f2c", [:mix], [{:hammer, "~> 6.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:redix, "~> 1.1", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "217ea066278910543a5e9b577d5bf2425419446b94fe76bdd9f255f39feec9fa"}, "hammer_plug": {:hex, :hammer_plug, "3.0.0", "7b1d000021e3ccc92cc6405c5537d3ec22f8f8f1274a1ae9351a8d98c32a2803", [:mix], [{:hammer, "~> 6.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "35275f98f887bef8d84a8e0a3021ba1cd14d0e15c11221f6f8c833a3d43f35d8"}, - "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"}, - "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, + "httpoison": {:hex, :httpoison, "3.0.0", "8566a933bb9175236d1ec335978445b67cd1f5b5d3ead6ca4b80be469d41f5d9", [:mix], [{:hackney, "~> 4.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "9130197b7658901c493d6fcfb842fb9676300fa8a6c8ed058c8889cf1a77f3c2"}, + "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "joken": {:hex, :joken, "2.7.0", "a9fd87805b1b58313435c04b950857d7557f019b157d503dc4783006e4e80ed1", [:mix], [{:jose, "~> 1.11.12", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "ffd0d92e12dbf497311386b75f9b5027d370e32c74f6e5576804d3a9eed668ab"}, + "jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"}, "jsx": {:hex, :jsx, "3.1.0", "d12516baa0bb23a59bb35dccaf02a1bd08243fcbb9efe24f2d9d056ccff71268", [:rebar3], [], "hexpm", "0c5cc8fdc11b53cc25cf65ac6705ad39e54ecc56d1c22e4adb8f5a53fb9427f3"}, "lager": {:hex, :lager, "3.9.2", "4cab289120eb24964e3886bd22323cb5fefe4510c076992a23ad18cf85413d8c", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm", "7f904d9e87a8cb7e66156ed31768d1c8e26eba1d54f4bc85b1aa4ac1f6340c28"}, "logger_json": {:hex, :logger_json, "7.0.4", "e315f2b9a755504658a745f3eab90d88d2cd7ac2ecfd08c8da94d8893965ab5c", [:mix], [{:decimal, ">= 0.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d1369f8094e372db45d50672c3b91e8888bcd695fdc444a37a0734e96717c45c"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, - "myxql": {:hex, :myxql, "0.6.3", "3d77683a09f1227abb8b73d66b275262235c5cae68182f0cfa5897d72a03700e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "af9eb517ddaced5c5c28e8749015493757fd4413f2cfccea449c466d405d9f51"}, + "myxql": {:hex, :myxql, "0.9.0", "1aec422f2c6b23215be0f00d84389f1ec27032d561ccf2927917e1bc541a6464", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4 or ~> 4.0", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "3a17b38562a7a995c291570875fd96c06b7b0369d6eb0592a52a9dccbe5d144b"}, "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, "oauth2": {:hex, :oauth2, "2.0.1", "70729503e05378697b958919bb2d65b002ba6b28c8112328063648a9348aaa3f", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "c64e20d4d105bcdbcbe03170fb530d0eddc3a3e6b135a87528a22c8aecf74c52"}, "optimal": {:hex, :optimal, "0.3.7", "d614c07dddef168c6c31fe45c12f2cd1b1a0ae827f639c7eb8e4c374c58f1854", [:mix], [], "hexpm", "d649ecd5208d9053b406f1ad85f2830698cf99734d75a4ba82a0027bb8538ce8"}, - "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "parse_trans": {:hex, :parse_trans, "3.4.2", "c352ddc1a0d5e54f9b1654d45f9c432eef76f9cea371c55ddff769ef688fdb74", [:rebar3], [], "hexpm", "4c25347de3b7c35732d32e69ab43d1ceee0beae3f3b3ade1b59cbd3dd224d9ca"}, "phoenix": {:hex, :phoenix, "1.8.14", "9279cbbcd755ac8d5f42d206915e7df13b9ab49096b98114f1c99ff908e5b42e", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 2.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "2782ff375824b2b5e41561fbae4764ee7b875af6898483bca49f24a9d1e37816"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, @@ -68,12 +69,13 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "phx_gen_auth": {:hex, :phx_gen_auth, "0.7.0", "2e10e9527b6b71abbfbb4601c7dc4aa4fb9f2db6f9a6be457c468b7f2b0f6319", [:mix], [{:phoenix, "~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b9dc3e3b866e67c5db8f00f4a2adb28fc8636e794f78600e35aba0e55bdac209"}, - "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, + "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.9.0", "87e21e0d9054ced99c36d128f49e3ea2cd8b745fffb97de50bff99706087af4f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2002bafba4f3a45b55a58e68d70211b153a7ed18d37edb1ceb6e96e7a92c422e"}, "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, "protobuf": {:hex, :protobuf, "0.17.0", "39e24e43c9648e148feba16ed51100b5b2028ea900b55460377b0476f6e10613", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ca6c91f6f63e2c147b47f03eefd10b80538aa6fc55ff4b12b795efb786b0152f"}, + "quic": {:hex, :quic, "1.8.2", "c315176d2c4fad0725e52f2a8033b96d01c4fecfc7e6a7333615ff10041142a5", [:rebar3], [], "hexpm", "274d2f41ee9c00d8d6415248df9fb5637381fe69b5387771dc37e91038a65479"}, "rabbit_common": {:hex, :rabbit_common, "4.3.4", "2911b2b563aa6a477c55bb1f4cbf83fcb7accc5412722b3bb5f3ae4e5e33f877", [:make, :rebar3], [{:credentials_obfuscation, "3.5.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:ranch, "2.2.0", [hex: :ranch, repo: "hexpm", optional: false]}, {:recon, "2.5.6", [hex: :recon, repo: "hexpm", optional: false]}, {:thoas, "1.2.1", [hex: :thoas, repo: "hexpm", optional: false]}], "hexpm", "e5fefde66701a0d6ce7cc464133c6975b0b2661b47561d7c622cce6b7420413c"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, @@ -94,4 +96,5 @@ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.6.0", "73db5ab8aaefd1a876a97ce3e6afc96562625de69ef17a4e04426e034849d0b8", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "50021a85bce8f203b086705d9e0c5415e2c7eb05d319111b0428fe71f9934617"}, + "webtransport": {:hex, :webtransport, "0.4.5", "0e387202bbe707389fe81373ef8c56faa9d5aa321bb4800fa4765ee7c1399785", [:rebar3], [{:h2, "~> 0.12", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 1.8.0", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "bcb512239e48e551d5bd5c667312a9a7de4f29b89d84b1d33c5af44e1f3f730d"}, } From e8e7c67756b1b2bb887cb12fc450f31eb203b2a0 Mon Sep 17 00:00:00 2001 From: Erin O'Connell Date: Mon, 14 Sep 2026 16:44:20 -0600 Subject: [PATCH 15/15] Bump credo to fix Elixir 1.18 tokenizer crash credo 1.7.1 crashes on every file under Elixir 1.18 (CaseClauseError: :elixir_tokenizer.tokenize/3 now returns a 6-tuple). Bumps to 1.7.19, already allowed by mix.exs's existing ~> 1.5 constraint. --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index b86e0755..6b649b50 100644 --- a/mix.lock +++ b/mix.lock @@ -3,7 +3,7 @@ "amqp_client": {:hex, :amqp_client, "4.3.4", "b7ce1678e1e57495d4a170403016faf3808c1301f588325c4024159391797c5a", [:make, :rebar3], [{:credentials_obfuscation, "3.5.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:rabbit_common, "4.3.4", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm", "61418e1ce1097ad87ce51833929c2c1243782f1ace2e49897d8cc5a85ba53753"}, "argon2_elixir": {:hex, :argon2_elixir, "2.4.1", "edb27bdd326bc738f3e4614eddc2f73507be6fedc9533c6bcc6f15bbac9c85cc", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "0e21f52a373739d00bdfd5fe6da2f04eea623cb4f66899f7526dd9db03903d9f"}, "bottle": {:git, "https://github.com/system76/bottle.git", "229a577bc24ce3e03278d084263fa2a4aab4d367", [ref: "229a577bc24ce3e03278d084263fa2a4aab4d367"]}, - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, "certifi": {:hex, :certifi, "2.17.0", "835748414307e15e05b17d0e518190228ce648b08d569a5cc93a85a40f3e5c9b", [:rebar3], [], "hexpm", "8122798a17f0293c80daada25d0f81c7f4d708c73fef782c7c9b1950e26e4d21"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, @@ -13,7 +13,7 @@ "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.20.0", "bb525377ba634cd6d68bac7bff5d98571f738806139d335daca827717c5dc172", [:make, :rebar3], [], "hexpm", "7d41a0dd2c093041ff3779ac5fe8a1585a68ec7cb2dd1de0536bdd2452fd7ba1"}, "credentials_obfuscation": {:hex, :credentials_obfuscation, "3.5.0", "61e282adfb4439486b3994faaec69543c7ee6cc7e70c6340e8853fd9deaf8219", [:rebar3], [], "hexpm", "843adbe3246861ce0f1a0fa3222f384834eb31defd8d6b9cba7afd2977c957bc"}, - "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, + "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"},