diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0041d1a..b742849 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 bec83f6..96a6121 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/BIGCOMMERCE_SYNC.md b/BIGCOMMERCE_SYNC.md new file mode 100644 index 0000000..eb08f83 --- /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/Dockerfile b/Dockerfile index b0c969d..217bd58 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/config.exs b/config/config.exs index f14b764..6b8ceb3 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 846b8f6..ecafe42 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,14 +1,15 @@ import Config config :recognizer, RecognizerWeb.Endpoint, + url: [scheme: "https", port: 443], http: [port: 8080], cache_static_manifest: "priv/static/cache_manifest.json", gzip: true, 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/config/releases.exs b/config/releases.exs index ca60e2b..6e2d675 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/config/test.exs b/config/test.exs index d359286..a50111f 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/accounts.ex b/lib/recognizer/accounts.ex index 4a42e1c..158d279 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 75a1f61..0ad6afc 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 7a6e54c..394f19a 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 @@ -249,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/caster.ex b/lib/recognizer/caster.ex index 90f837c..e958267 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/hal.ex b/lib/recognizer/hal.ex index 733a13d..de8807d 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 @@ -71,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) @@ -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 @@ -183,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) @@ -195,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/notifications/account.ex b/lib/recognizer/notifications/account.ex index 839efb9..62ce440 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/lib/recognizer_web.ex b/lib/recognizer_web.ex index 678e80e..ae94712 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/lib/recognizer_web/authentication.ex b/lib/recognizer_web/authentication.ex index b4c1693..c88598f 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. """ @@ -147,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)] @@ -158,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/controllers/accounts/api/user_settings_two_factor_controller.ex b/lib/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller.ex index 0bb9005..e685580 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/accounts/prompt/two_factor_controller.ex b/lib/recognizer_web/controllers/accounts/prompt/two_factor_controller.ex index 526d8cf..c4942ad 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 26461b4..089aa88 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} -> @@ -34,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 @@ -74,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 @@ -137,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 diff --git a/lib/recognizer_web/controllers/accounts/user_session_controller.ex b/lib/recognizer_web/controllers/accounts/user_session_controller.ex index 17bd3cf..51d0777 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 6cc207d..606c5dd 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/lib/recognizer_web/controllers/fallback_controller.ex b/lib/recognizer_web/controllers/fallback_controller.ex index 8a3d4b3..8a977f6 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" diff --git a/lib/recognizer_web/controllers/oauth_provider/authorize_controller.ex b/lib/recognizer_web/controllers/oauth_provider/authorize_controller.ex index bb6c499..26ebe94 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 e6731c2..a16ce9b 100644 --- a/lib/recognizer_web/controllers/oauth_provider/token_controller.ex +++ b/lib/recognizer_web/controllers/oauth_provider/token_controller.ex @@ -3,6 +3,45 @@ defmodule RecognizerWeb.OauthProvider.TokenController do alias ExOauth2Provider.Token + @one_minute 60_000 + + # 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, 100}, + by: {:conn, &__MODULE__.get_rate_limit_key/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 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} -> @@ -25,4 +64,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/endpoint.ex b/lib/recognizer_web/endpoint.ex index e44b2c9..7281a8b 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/router.ex b/lib/recognizer_web/router.ex index d5cad9a..f7955e1 100644 --- a/lib/recognizer_web/router.ex +++ b/lib/recognizer_web/router.ex @@ -65,11 +65,15 @@ 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 [:api] + pipe_through [:browser, :bc, :auth, :user] - post "/oauth/token", TokenController, :create - match :*, "/oauth/token", TokenController, :method_not_allowed + 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 @@ -84,15 +88,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] @@ -143,4 +138,15 @@ defmodule RecognizerWeb.Router do post "/settings/two-factor", UserSettingsController, :two_factor_confirm get "/setting/two-factor/resend", UserSettingsController, :resend 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 diff --git a/lib/recognizer_web/telemetry.ex b/lib/recognizer_web/telemetry.ex index b6d296b..b15f337 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 38efc90..761e03f 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 @@ -34,10 +35,10 @@ 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}, + {: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"}, @@ -48,15 +49,18 @@ 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"}, - {:logger_json, github: "Nebo15/logger_json", ref: "8e4290a"}, + {:joken, "~> 2.7"}, + {:logger_json, "~> 7.0"}, {:myxql, ">= 0.0.0"}, {:redix, ">= 0.0.0"}, {:phoenix_ecto, "~> 4.1"}, @@ -64,12 +68,12 @@ 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"}, - {:plug_cowboy, "~> 2.4"}, + {:phoenix, "~> 1.8"}, + {:plug_cowboy, "~> 2.9"}, {: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 9dd9104..6b649b5 100644 --- a/mix.lock +++ b/mix.lock @@ -1,25 +1,25 @@ %{ - "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"]}, - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "bottle": {:git, "https://github.com/system76/bottle.git", "229a577bc24ce3e03278d084263fa2a4aab4d367", [ref: "229a577bc24ce3e03278d084263fa2a4aab4d367"]}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "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"}, - "credentials_obfuscation": {:hex, :credentials_obfuscation, "3.4.0", "34e18b126b3aefd6e8143776fbe1ceceea6792307c99ac5ee8687911f048cfd7", [:rebar3], [], "hexpm", "738ace0ed5545d2710d3f7383906fc6f6b582d019036e5269c4dbd85dbced566"}, - "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"}, + "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.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, "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"}, @@ -30,66 +30,71 @@ "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"}, - "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"}, + "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.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": {: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"}, + "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.2.4", "7b3790ef684089076b63c0f08c2f4b079c6311daeb006b69e4ed2bf67518291e", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b351b6d992d79624a8430a99d21a41b36b1b90edf84326a294e9f4a2de11f089"}, - "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"}, + "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, + "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.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"}, - "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"}, + "optimal": {:hex, :optimal, "0.3.7", "d614c07dddef168c6c31fe45c12f2cd1b1a0ae827f639c7eb8e4c374c58f1854", [:mix], [], "hexpm", "d649ecd5208d9053b406f1ad85f2830698cf99734d75a4ba82a0027bb8538ce8"}, + "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.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": {: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.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"}, + "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"}, "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"}, "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"}, + "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"}, }