diff --git a/assets/resume/kyle-neal-resume.pdf b/assets/resume/kyle-neal-resume.pdf index 22159ac..debf026 100644 Binary files a/assets/resume/kyle-neal-resume.pdf and b/assets/resume/kyle-neal-resume.pdf differ diff --git a/config/runtime.exs b/config/runtime.exs index 87882b6..ce0927b 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -24,6 +24,8 @@ config :revstack, RevstackWeb.Endpoint, http: [port: String.to_integer(System.get_env("PORT", "4000"))] if config_env() == :prod do + fly_app_name = System.get_env("FLY_APP_NAME") + config :revstack, Revstack.Repo, Revstack.RepoConfig.prod_repo_config() # The secret key base is used to sign/encrypt cookies and other secrets. @@ -66,6 +68,11 @@ if config_env() == :prod do System.get_env("ADMIN_PASSWORD") || raise("Missing environment variable `ADMIN_PASSWORD`!") + config :revstack, RevstackWeb.Plugs.FlyRedirect, + enabled: not is_nil(fly_app_name), + source_host: "revstack.fly.dev", + target_url: "https://revenuelink.net" + # Trust proxy headers (X-Forwarded-For) from Fly.io load balancer config :revstack, trust_proxy: true diff --git a/fly.toml b/fly.toml index bfa09b0..6301442 100644 --- a/fly.toml +++ b/fly.toml @@ -7,6 +7,7 @@ app = "revstack" primary_region = "dfw" kill_signal = "SIGTERM" kill_timeout = "5s" +swap_size_mb = 512 [build] @@ -22,17 +23,11 @@ kill_timeout = "5s" [http_service] internal_port = 8080 force_https = true - auto_stop_machines = "stop" + auto_stop_machines = true auto_start_machines = true - min_machines_running = 1 + min_machines_running = 0 processes = ["app"] - [http_service.concurrency] type = "connections" hard_limit = 1000 soft_limit = 1000 - -[[vm]] - memory = "1gb" - cpu_kind = "shared" - cpus = 1 diff --git a/lib/revstack_web/endpoint.ex b/lib/revstack_web/endpoint.ex index 19c3438..ea69eaf 100644 --- a/lib/revstack_web/endpoint.ex +++ b/lib/revstack_web/endpoint.ex @@ -15,6 +15,8 @@ defmodule RevstackWeb.Endpoint do websocket: [connect_info: [session: @session_options]], longpoll: [connect_info: [session: @session_options]] + plug RevstackWeb.Plugs.FlyRedirect + # Serve at "/" the static files from "priv/static" directory. # # When code reloading is disabled (e.g., in production), diff --git a/lib/revstack_web/plugs/fly_redirect.ex b/lib/revstack_web/plugs/fly_redirect.ex new file mode 100644 index 0000000..2664b36 --- /dev/null +++ b/lib/revstack_web/plugs/fly_redirect.ex @@ -0,0 +1,36 @@ +defmodule RevstackWeb.Plugs.FlyRedirect do + @moduledoc """ + Redirects requests from the Fly app domain to the canonical external site. + + This plug is intended to run in production and is controlled via runtime config. + """ + + @behaviour Plug + + import Plug.Conn + + @impl true + def init(opts), do: opts + + @impl true + def call(conn, _opts) do + redirect_config = Application.get_env(:revstack, __MODULE__, []) + + enabled? = Keyword.get(redirect_config, :enabled, false) + source_host = Keyword.get(redirect_config, :source_host, "revstack.fly.dev") + target_url = Keyword.get(redirect_config, :target_url, "https://revenuelink.net") + + if enabled? and host_matches?(conn.host, source_host) do + conn + |> put_resp_header("location", target_url) + |> send_resp(301, "") + |> halt() + else + conn + end + end + + defp host_matches?(host, source_host) do + String.downcase(host || "") == String.downcase(source_host || "") + end +end diff --git a/mix.exs b/mix.exs index 6c98c5c..b3e7a3b 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Revstack.MixProject do def project do [ app: :revstack, - version: "1.4.0", + version: "1.5.0", elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, diff --git a/test/revstack_web/plugs/fly_redirect_test.exs b/test/revstack_web/plugs/fly_redirect_test.exs new file mode 100644 index 0000000..e27d2f6 --- /dev/null +++ b/test/revstack_web/plugs/fly_redirect_test.exs @@ -0,0 +1,68 @@ +defmodule RevstackWeb.Plugs.FlyRedirectTest do + use RevstackWeb.ConnCase, async: true + + alias RevstackWeb.Plugs.FlyRedirect + + describe "call/2" do + setup do + original_config = Application.get_env(:revstack, FlyRedirect, []) + + on_exit(fn -> + Application.put_env(:revstack, FlyRedirect, original_config) + end) + + :ok + end + + test "redirects when enabled and host matches source host", %{conn: conn} do + Application.put_env(:revstack, FlyRedirect, + enabled: true, + source_host: "revstack.fly.dev", + target_url: "https://revenuelink.net" + ) + + conn = + conn + |> Map.put(:host, "revstack.fly.dev") + |> FlyRedirect.call([]) + + assert conn.halted + assert conn.status == 301 + assert get_resp_header(conn, "location") == ["https://revenuelink.net"] + end + + test "does not redirect when disabled", %{conn: conn} do + Application.put_env(:revstack, FlyRedirect, + enabled: false, + source_host: "revstack.fly.dev", + target_url: "https://revenuelink.net" + ) + + conn = + conn + |> Map.put(:host, "revstack.fly.dev") + |> FlyRedirect.call([]) + + refute conn.halted + assert conn.status == nil + assert get_resp_header(conn, "location") == [] + end + + test "does not redirect for non-matching host", %{conn: conn} do + Application.put_env(:revstack, FlyRedirect, + enabled: true, + source_host: "revstack.fly.dev", + target_url: "https://revenuelink.net" + ) + + conn = + conn + |> Map.put(:host, "example.com") + |> FlyRedirect.call([]) + + refute conn.halted + assert conn.status == nil + assert get_resp_header(conn, "location") == [] + end + end +end