From 83539e8c7dc1c4acc9569efb7f1e272428204596 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Wed, 3 Apr 2024 14:12:16 -0400 Subject: [PATCH 01/38] Added modules needed to keep live alerts in memory. --- lib/screenplay/alerts/cache/fetcher.ex | 46 ++++++++++++++++++ lib/screenplay/alerts/cache/store.ex | 59 +++++++++++++++++++++++ lib/screenplay/alerts/cache/supervisor.ex | 14 ++++++ 3 files changed, 119 insertions(+) create mode 100644 lib/screenplay/alerts/cache/fetcher.ex create mode 100644 lib/screenplay/alerts/cache/store.ex create mode 100644 lib/screenplay/alerts/cache/supervisor.ex diff --git a/lib/screenplay/alerts/cache/fetcher.ex b/lib/screenplay/alerts/cache/fetcher.ex new file mode 100644 index 00000000..5a2b0649 --- /dev/null +++ b/lib/screenplay/alerts/cache/fetcher.ex @@ -0,0 +1,46 @@ +defmodule Screenplay.Alerts.Cache.Fetcher do + require Logger + + use GenServer + + alias Screenplay.Alerts.Alert + alias Screenplay.Alerts.Cache.Store + + @update_interval_ms 4_000 + + # Client + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, opts) + end + + # Server + @impl true + def init(state) do + schedule_fetch(@update_interval_ms) + + {:ok, state} + end + + @impl true + def handle_info(:fetch, state) do + case Alert.fetch() do + {:ok, alerts} -> + Store.update(alerts) + + :error -> + _ = Logger.info("#{__MODULE__} error fetching alerts") + end + + schedule_fetch(@update_interval_ms) + + {:noreply, state, :hibernate} + end + + def handle_info(_, state) do + {:noreply, state} + end + + defp schedule_fetch(ms) do + Process.send_after(self(), :fetch, ms) + end +end diff --git a/lib/screenplay/alerts/cache/store.ex b/lib/screenplay/alerts/cache/store.ex new file mode 100644 index 00000000..7ab118ff --- /dev/null +++ b/lib/screenplay/alerts/cache/store.ex @@ -0,0 +1,59 @@ +defmodule Screenplay.Alerts.Cache.Store do + use GenServer + + alias Screenplay.Alerts.Alert + + # Client + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + @doc """ + Sets the ETS cache to these set of alerts. + The previous alerts in the cache are all removed. + """ + @spec update(list(Alert.t()) | nil) :: :ok + def update(alerts) do + GenServer.call(__MODULE__, {:update, alerts}) + end + + @doc """ + Retrieves an alert struct given an alert ID. + """ + @spec alert(String.t()) :: Alert.t() + def alert(alert_id) do + case :ets.match(:alerts, {alert_id, :"$1"}) do + [[alert]] -> alert + [] -> nil + end + end + + @doc """ + Retrieves the full set of alerts. + """ + @spec alerts() :: list(Alert.t()) + def alerts() do + :ets.select(:alerts, [{{:_, :"$1"}, [], [:"$1"]}]) + end + + # Server + @impl true + def init(_args) do + _ = :ets.new(:alerts, [:protected, :named_table, read_concurrency: true]) + {:ok, []} + end + + @impl true + def handle_call({:update, alerts}, _from, state) do + alerts_to_insert = + Enum.reduce(alerts, [], fn alert, acc -> + [{alert.id, alert} | acc] + end) + + :ets.delete_all_objects(:alerts) + :ets.insert(:alerts, alerts_to_insert) + + {:reply, :ok, state, :hibernate} + end +end diff --git a/lib/screenplay/alerts/cache/supervisor.ex b/lib/screenplay/alerts/cache/supervisor.ex new file mode 100644 index 00000000..69c0090f --- /dev/null +++ b/lib/screenplay/alerts/cache/supervisor.ex @@ -0,0 +1,14 @@ +defmodule Screenplay.Alerts.Cache.Supervisor do + use Supervisor + + def start_link(_) do + Supervisor.start_link(__MODULE__, []) + end + + @impl Supervisor + def init(_arg) do + children = [Screenplay.Alerts.Cache.Store, Screenplay.Alerts.Cache.Fetcher] + + Supervisor.init(children, strategy: :rest_for_one) + end +end From 6cee1c90f13ca7060e3718a455c34e92c3f4f1e1 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Wed, 3 Apr 2024 14:12:30 -0400 Subject: [PATCH 02/38] Added new processes to supervision tree. --- lib/screenplay/application.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/screenplay/application.ex b/lib/screenplay/application.ex index 1623d6c8..c44cb02f 100644 --- a/lib/screenplay/application.ex +++ b/lib/screenplay/application.ex @@ -17,7 +17,8 @@ defmodule Screenplay.Application do # {Screenplay.Worker, arg} Screenplay.OutfrontTakeoverTool.Alerts.State, Screenplay.OutfrontTakeoverTool.Alerts.Reminders, - Screenplay.Scheduler + Screenplay.Scheduler, + Screenplay.Alerts.Cache.Supervisor ] # See https://hexdocs.pm/elixir/Supervisor.html From a26e2660776dd40850c050e8749c1a68fde6f2a8 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Thu, 4 Apr 2024 15:59:06 -0400 Subject: [PATCH 03/38] Added store test module. --- test/screenplay/alerts/cache/store_test.exs | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/screenplay/alerts/cache/store_test.exs diff --git a/test/screenplay/alerts/cache/store_test.exs b/test/screenplay/alerts/cache/store_test.exs new file mode 100644 index 00000000..847b3983 --- /dev/null +++ b/test/screenplay/alerts/cache/store_test.exs @@ -0,0 +1,41 @@ +defmodule Screenplay.Alerts.Cache.StoreTest do + use ExUnit.Case + + alias Screenplay.Alerts.Alert + alias Screenplay.Alerts.Cache.Store + + setup_all do + _ = start_supervised(Store) + :ok + end + + test "updating and fetching" do + alert1 = + struct(Alert, + id: "123", + informed_entities: [ + %{route: "Blue"}, + %{stop: "place-pktrm"} + ] + ) + + alert2 = + struct(Alert, + id: "456", + informed_entities: [ + %{route: "Red"} + ] + ) + + alerts = [alert1, alert2] + + Store.update(alerts) + + expected_alerts = MapSet.new(alerts) + actual_alerts = MapSet.new(Store.alerts()) + + assert expected_alerts == actual_alerts + assert Store.alert("123") == alert1 + assert Store.alert("aaa") == nil + end +end From a6727801838991dbd92fdfe772df02272299bae5 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Thu, 4 Apr 2024 16:21:18 -0400 Subject: [PATCH 04/38] Made fetcher more testable. --- lib/screenplay/alerts/cache/fetcher.ex | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/screenplay/alerts/cache/fetcher.ex b/lib/screenplay/alerts/cache/fetcher.ex index 5a2b0649..c55ab200 100644 --- a/lib/screenplay/alerts/cache/fetcher.ex +++ b/lib/screenplay/alerts/cache/fetcher.ex @@ -3,35 +3,45 @@ defmodule Screenplay.Alerts.Cache.Fetcher do use GenServer + alias Screenplay.V3Api alias Screenplay.Alerts.Alert alias Screenplay.Alerts.Cache.Store - @update_interval_ms 4_000 + @default_opts [ + get_json_fn: &V3Api.get_json/2, + update_interval_ms: 4_000, + update_fn: &Store.update/1 + ] # Client def start_link(opts \\ []) do + opts = Keyword.merge(@default_opts, opts) GenServer.start_link(__MODULE__, opts) end # Server @impl true - def init(state) do - schedule_fetch(@update_interval_ms) + def init(opts) do + get_json_fn = Keyword.get(opts, :get_json_fn) + update_interval_ms = Keyword.get(opts, :update_interval_ms) + update_fn = Keyword.get(opts, :update_fn) - {:ok, state} + schedule_fetch(update_interval_ms) + + {:ok, {get_json_fn, update_interval_ms, update_fn}} end @impl true - def handle_info(:fetch, state) do - case Alert.fetch() do + def handle_info(:fetch, {get_json_fn, update_interval_ms, update_fn} = state) do + case Alert.fetch(get_json_fn) do {:ok, alerts} -> - Store.update(alerts) + update_fn.(alerts) :error -> _ = Logger.info("#{__MODULE__} error fetching alerts") end - schedule_fetch(@update_interval_ms) + schedule_fetch(update_interval_ms) {:noreply, state, :hibernate} end From 4d8eeccd3e21f3bb99d61442e959cfbef36b54b4 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Fri, 5 Apr 2024 11:22:29 -0400 Subject: [PATCH 05/38] Added Fetcher test module. --- test/screenplay/alerts/cache/fetcher_test.exs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 test/screenplay/alerts/cache/fetcher_test.exs diff --git a/test/screenplay/alerts/cache/fetcher_test.exs b/test/screenplay/alerts/cache/fetcher_test.exs new file mode 100644 index 00000000..9f493952 --- /dev/null +++ b/test/screenplay/alerts/cache/fetcher_test.exs @@ -0,0 +1,93 @@ +defmodule Screenplay.Alerts.Cache.FetcherTest do + use ExUnit.Case + + alias Screenplay.Alerts.Alert + alias Screenplay.Alerts.Cache.Fetcher + + defp alert_json(id) do + %{ + "id" => id, + "attributes" => %{ + "active_period" => [], + "created_at" => nil, + "updated_at" => nil, + "cause" => nil, + "effect" => nil, + "header" => nil, + "informed_entity" => [], + "lifecycle" => nil, + "severity" => nil, + "timeframe" => nil, + "url" => nil, + "description" => nil + } + } + end + + test "It periodically polls an API and updates the store" do + {:ok, fake_store} = Agent.start_link(fn -> [] end) + + update_fn = fn alerts -> + Agent.update(fake_store, fn updates -> [alerts | updates] end) + send(self(), :updated) + end + + get_json_fn = fn "alerts", %{"include" => "routes"} -> + {:ok, %{"data" => [alert_json("1")], "included" => []}} + end + + {:ok, fetcher} = + Fetcher.start_link( + get_json_fn: get_json_fn, + update_fn: update_fn, + update_interval_ms: 10_000 + ) + + send(fetcher, :fetch) + send(fetcher, :fetch) + send(fetcher, :fetch) + _ = await_updated() + _ = await_updated() + _ = await_updated() + + alerts = Agent.get(fake_store, & &1) + + assert [[%Alert{id: "1"}], [%Alert{id: "1"}], [%Alert{id: "1"}]] = alerts + end + + test "It handles a failed API response and does not update the store" do + {:ok, fake_store} = Agent.start_link(fn -> [] end) + + update_fn = fn alerts -> + Agent.update(fake_store, fn updates -> [alerts | updates] end) + send(self(), :updated) + end + + get_json_fn = fn "alerts", %{"include" => "routes"} -> + :error + end + + {:ok, fetcher} = + Fetcher.start_link( + get_json_fn: get_json_fn, + update_fn: update_fn, + update_interval_ms: 10_000 + ) + + send(fetcher, :fetch) + send(fetcher, :fetch) + _ = await_updated() + _ = await_updated() + + alerts = Agent.get(fake_store, & &1) + assert alerts == [] + end + + defp await_updated do + receive do + :updated -> :ok + after + 100 -> :no_msg + end + end +end From 4cfd5df4a3c03a436d756e38fde6717b4fe2bfc9 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Fri, 5 Apr 2024 11:30:14 -0400 Subject: [PATCH 06/38] Added moduledocs. --- lib/screenplay/alerts/cache/fetcher.ex | 4 ++++ lib/screenplay/alerts/cache/store.ex | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/lib/screenplay/alerts/cache/fetcher.ex b/lib/screenplay/alerts/cache/fetcher.ex index c55ab200..b1714e29 100644 --- a/lib/screenplay/alerts/cache/fetcher.ex +++ b/lib/screenplay/alerts/cache/fetcher.ex @@ -1,4 +1,8 @@ defmodule Screenplay.Alerts.Cache.Fetcher do + @moduledoc """ + Module used to fetch current alerts from the V3 API every 4 seconds. Each set of alerts is passed to the Store module. + """ + require Logger use GenServer diff --git a/lib/screenplay/alerts/cache/store.ex b/lib/screenplay/alerts/cache/store.ex index 7ab118ff..7c998fe9 100644 --- a/lib/screenplay/alerts/cache/store.ex +++ b/lib/screenplay/alerts/cache/store.ex @@ -1,4 +1,9 @@ defmodule Screenplay.Alerts.Cache.Store do + @moduledoc """ + Module for storing alerts in an ETS table. Each call to `update/1` removes all existing alerts and replaces with a new list each time. + This will ensure that no expired alerts are stored in the table. + """ + use GenServer alias Screenplay.Alerts.Alert From 0d1d3b8acaccd443daf6fb06f67dfbcd11157765 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Fri, 5 Apr 2024 11:33:29 -0400 Subject: [PATCH 07/38] Credo. --- lib/screenplay/alerts/cache/fetcher.ex | 4 ++-- lib/screenplay/alerts/cache/store.ex | 4 ++-- lib/screenplay/alerts/cache/supervisor.ex | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/screenplay/alerts/cache/fetcher.ex b/lib/screenplay/alerts/cache/fetcher.ex index b1714e29..55a04981 100644 --- a/lib/screenplay/alerts/cache/fetcher.ex +++ b/lib/screenplay/alerts/cache/fetcher.ex @@ -7,9 +7,9 @@ defmodule Screenplay.Alerts.Cache.Fetcher do use GenServer - alias Screenplay.V3Api alias Screenplay.Alerts.Alert alias Screenplay.Alerts.Cache.Store + alias Screenplay.V3Api @default_opts [ get_json_fn: &V3Api.get_json/2, @@ -36,7 +36,7 @@ defmodule Screenplay.Alerts.Cache.Fetcher do end @impl true - def handle_info(:fetch, {get_json_fn, update_interval_ms, update_fn} = state) do + def handle_info(:fetch, state = {get_json_fn, update_interval_ms, update_fn}) do case Alert.fetch(get_json_fn) do {:ok, alerts} -> update_fn.(alerts) diff --git a/lib/screenplay/alerts/cache/store.ex b/lib/screenplay/alerts/cache/store.ex index 7c998fe9..71fc1fc0 100644 --- a/lib/screenplay/alerts/cache/store.ex +++ b/lib/screenplay/alerts/cache/store.ex @@ -37,8 +37,8 @@ defmodule Screenplay.Alerts.Cache.Store do @doc """ Retrieves the full set of alerts. """ - @spec alerts() :: list(Alert.t()) - def alerts() do + @spec alerts :: list(Alert.t()) + def alerts do :ets.select(:alerts, [{{:_, :"$1"}, [], [:"$1"]}]) end diff --git a/lib/screenplay/alerts/cache/supervisor.ex b/lib/screenplay/alerts/cache/supervisor.ex index 69c0090f..e2c786c4 100644 --- a/lib/screenplay/alerts/cache/supervisor.ex +++ b/lib/screenplay/alerts/cache/supervisor.ex @@ -1,4 +1,8 @@ defmodule Screenplay.Alerts.Cache.Supervisor do + @moduledoc """ + Module used to spin up processes need to cache current alerts fetched from the V3 API. + """ + use Supervisor def start_link(_) do From 75deb87dae36f1cd77b4a57b737a3ec6588ea988 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Tue, 9 Apr 2024 13:52:23 -0400 Subject: [PATCH 08/38] Consolidated logic so fetching and storing happens in the same module. --- config/config.exs | 3 +- .../alerts/{cache/fetcher.ex => cache.ex} | 48 +++++++--- lib/screenplay/alerts/cache/store.ex | 64 ------------- lib/screenplay/alerts/cache/supervisor.ex | 18 ---- lib/screenplay/application.ex | 31 ++++--- test/screenplay/alerts/cache/fetcher_test.exs | 93 ------------------- test/screenplay/alerts/cache/store_test.exs | 41 -------- test/screenplay/alerts/cache_test.exs | 64 +++++++++++++ 8 files changed, 117 insertions(+), 245 deletions(-) rename lib/screenplay/alerts/{cache/fetcher.ex => cache.ex} (51%) delete mode 100644 lib/screenplay/alerts/cache/store.ex delete mode 100644 lib/screenplay/alerts/cache/supervisor.ex delete mode 100644 test/screenplay/alerts/cache/fetcher_test.exs delete mode 100644 test/screenplay/alerts/cache/store_test.exs create mode 100644 test/screenplay/alerts/cache_test.exs diff --git a/config/config.exs b/config/config.exs index 6a209804..60540094 100644 --- a/config/config.exs +++ b/config/config.exs @@ -18,7 +18,8 @@ config :screenplay, ScreenplayWeb.Endpoint, config :screenplay, config_fetcher: Screenplay.Config.S3Fetch, config_s3_bucket: "mbta-ctd-config", - record_sentry: false + record_sentry: false, + start_alerts_cache: config_env() != :test # Include 2 logger backends config :logger, diff --git a/lib/screenplay/alerts/cache/fetcher.ex b/lib/screenplay/alerts/cache.ex similarity index 51% rename from lib/screenplay/alerts/cache/fetcher.ex rename to lib/screenplay/alerts/cache.ex index 55a04981..33e600cc 100644 --- a/lib/screenplay/alerts/cache/fetcher.ex +++ b/lib/screenplay/alerts/cache.ex @@ -1,6 +1,6 @@ -defmodule Screenplay.Alerts.Cache.Fetcher do +defmodule Screenplay.Alerts.Cache do @moduledoc """ - Module used to fetch current alerts from the V3 API every 4 seconds. Each set of alerts is passed to the Store module. + Module used to fetch current alerts from the V3 API every 4 seconds. Each set of alerts is saved to an internal ETS table. """ require Logger @@ -8,38 +8,62 @@ defmodule Screenplay.Alerts.Cache.Fetcher do use GenServer alias Screenplay.Alerts.Alert - alias Screenplay.Alerts.Cache.Store alias Screenplay.V3Api @default_opts [ get_json_fn: &V3Api.get_json/2, - update_interval_ms: 4_000, - update_fn: &Store.update/1 + update_interval_ms: 4_000 ] # Client - def start_link(opts \\ []) do + def start_link(opts) do opts = Keyword.merge(@default_opts, opts) GenServer.start_link(__MODULE__, opts) end + @doc """ + Retrieves an alert struct given an alert ID. + """ + @spec alert(String.t()) :: Alert.t() + def alert(alert_id) do + case :ets.match(:alerts, {alert_id, :"$1"}) do + [[alert]] -> alert + [] -> nil + end + end + + @doc """ + Retrieves the full set of alerts. + """ + @spec alerts :: list(Alert.t()) + def alerts do + :ets.select(:alerts, [{{:_, :"$1"}, [], [:"$1"]}]) + end + # Server @impl true def init(opts) do + _ = :ets.new(:alerts, [:protected, :named_table, read_concurrency: true]) + get_json_fn = Keyword.get(opts, :get_json_fn) update_interval_ms = Keyword.get(opts, :update_interval_ms) - update_fn = Keyword.get(opts, :update_fn) schedule_fetch(update_interval_ms) - {:ok, {get_json_fn, update_interval_ms, update_fn}} + {:ok, {get_json_fn, update_interval_ms}} end @impl true - def handle_info(:fetch, state = {get_json_fn, update_interval_ms, update_fn}) do + def handle_info(:fetch, state = {get_json_fn, update_interval_ms}) do case Alert.fetch(get_json_fn) do {:ok, alerts} -> - update_fn.(alerts) + alerts_to_insert = + Enum.reduce(alerts, [], fn alert, acc -> + [{alert.id, alert} | acc] + end) + + :ets.delete_all_objects(:alerts) + :ets.insert(:alerts, alerts_to_insert) :error -> _ = Logger.info("#{__MODULE__} error fetching alerts") @@ -50,10 +74,6 @@ defmodule Screenplay.Alerts.Cache.Fetcher do {:noreply, state, :hibernate} end - def handle_info(_, state) do - {:noreply, state} - end - defp schedule_fetch(ms) do Process.send_after(self(), :fetch, ms) end diff --git a/lib/screenplay/alerts/cache/store.ex b/lib/screenplay/alerts/cache/store.ex deleted file mode 100644 index 71fc1fc0..00000000 --- a/lib/screenplay/alerts/cache/store.ex +++ /dev/null @@ -1,64 +0,0 @@ -defmodule Screenplay.Alerts.Cache.Store do - @moduledoc """ - Module for storing alerts in an ETS table. Each call to `update/1` removes all existing alerts and replaces with a new list each time. - This will ensure that no expired alerts are stored in the table. - """ - - use GenServer - - alias Screenplay.Alerts.Alert - - # Client - - def start_link(_) do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - @doc """ - Sets the ETS cache to these set of alerts. - The previous alerts in the cache are all removed. - """ - @spec update(list(Alert.t()) | nil) :: :ok - def update(alerts) do - GenServer.call(__MODULE__, {:update, alerts}) - end - - @doc """ - Retrieves an alert struct given an alert ID. - """ - @spec alert(String.t()) :: Alert.t() - def alert(alert_id) do - case :ets.match(:alerts, {alert_id, :"$1"}) do - [[alert]] -> alert - [] -> nil - end - end - - @doc """ - Retrieves the full set of alerts. - """ - @spec alerts :: list(Alert.t()) - def alerts do - :ets.select(:alerts, [{{:_, :"$1"}, [], [:"$1"]}]) - end - - # Server - @impl true - def init(_args) do - _ = :ets.new(:alerts, [:protected, :named_table, read_concurrency: true]) - {:ok, []} - end - - @impl true - def handle_call({:update, alerts}, _from, state) do - alerts_to_insert = - Enum.reduce(alerts, [], fn alert, acc -> - [{alert.id, alert} | acc] - end) - - :ets.delete_all_objects(:alerts) - :ets.insert(:alerts, alerts_to_insert) - - {:reply, :ok, state, :hibernate} - end -end diff --git a/lib/screenplay/alerts/cache/supervisor.ex b/lib/screenplay/alerts/cache/supervisor.ex deleted file mode 100644 index e2c786c4..00000000 --- a/lib/screenplay/alerts/cache/supervisor.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Screenplay.Alerts.Cache.Supervisor do - @moduledoc """ - Module used to spin up processes need to cache current alerts fetched from the V3 API. - """ - - use Supervisor - - def start_link(_) do - Supervisor.start_link(__MODULE__, []) - end - - @impl Supervisor - def init(_arg) do - children = [Screenplay.Alerts.Cache.Store, Screenplay.Alerts.Cache.Fetcher] - - Supervisor.init(children, strategy: :rest_for_one) - end -end diff --git a/lib/screenplay/application.ex b/lib/screenplay/application.ex index c44cb02f..072b9d42 100644 --- a/lib/screenplay/application.ex +++ b/lib/screenplay/application.ex @@ -6,20 +6,23 @@ defmodule Screenplay.Application do use Application def start(_type, _args) do - children = [ - # Start the Telemetry supervisor - ScreenplayWeb.Telemetry, - # Start the PubSub system - {Phoenix.PubSub, name: Screenplay.PubSub}, - # Start the Endpoint (http/https) - ScreenplayWeb.Endpoint, - # Start a worker by calling: Screenplay.Worker.start_link(arg) - # {Screenplay.Worker, arg} - Screenplay.OutfrontTakeoverTool.Alerts.State, - Screenplay.OutfrontTakeoverTool.Alerts.Reminders, - Screenplay.Scheduler, - Screenplay.Alerts.Cache.Supervisor - ] + children = + [ + # Start the Telemetry supervisor + ScreenplayWeb.Telemetry, + # Start the PubSub system + {Phoenix.PubSub, name: Screenplay.PubSub}, + # Start the Endpoint (http/https) + ScreenplayWeb.Endpoint, + Screenplay.OutfrontTakeoverTool.Alerts.State, + Screenplay.OutfrontTakeoverTool.Alerts.Reminders, + Screenplay.Scheduler + ] ++ + if Application.get_env(:screenplay, :start_alerts_cache) do + [Screenplay.Alerts.Cache] + else + [] + end # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options diff --git a/test/screenplay/alerts/cache/fetcher_test.exs b/test/screenplay/alerts/cache/fetcher_test.exs deleted file mode 100644 index 9f493952..00000000 --- a/test/screenplay/alerts/cache/fetcher_test.exs +++ /dev/null @@ -1,93 +0,0 @@ -defmodule Screenplay.Alerts.Cache.FetcherTest do - use ExUnit.Case - - alias Screenplay.Alerts.Alert - alias Screenplay.Alerts.Cache.Fetcher - - defp alert_json(id) do - %{ - "id" => id, - "attributes" => %{ - "active_period" => [], - "created_at" => nil, - "updated_at" => nil, - "cause" => nil, - "effect" => nil, - "header" => nil, - "informed_entity" => [], - "lifecycle" => nil, - "severity" => nil, - "timeframe" => nil, - "url" => nil, - "description" => nil - } - } - end - - test "It periodically polls an API and updates the store" do - {:ok, fake_store} = Agent.start_link(fn -> [] end) - - update_fn = fn alerts -> - Agent.update(fake_store, fn updates -> [alerts | updates] end) - send(self(), :updated) - end - - get_json_fn = fn "alerts", %{"include" => "routes"} -> - {:ok, %{"data" => [alert_json("1")], "included" => []}} - end - - {:ok, fetcher} = - Fetcher.start_link( - get_json_fn: get_json_fn, - update_fn: update_fn, - update_interval_ms: 10_000 - ) - - send(fetcher, :fetch) - send(fetcher, :fetch) - send(fetcher, :fetch) - _ = await_updated() - _ = await_updated() - _ = await_updated() - - alerts = Agent.get(fake_store, & &1) - - assert [[%Alert{id: "1"}], [%Alert{id: "1"}], [%Alert{id: "1"}]] = alerts - end - - test "It handles a failed API response and does not update the store" do - {:ok, fake_store} = Agent.start_link(fn -> [] end) - - update_fn = fn alerts -> - Agent.update(fake_store, fn updates -> [alerts | updates] end) - send(self(), :updated) - end - - get_json_fn = fn "alerts", %{"include" => "routes"} -> - :error - end - - {:ok, fetcher} = - Fetcher.start_link( - get_json_fn: get_json_fn, - update_fn: update_fn, - update_interval_ms: 10_000 - ) - - send(fetcher, :fetch) - send(fetcher, :fetch) - _ = await_updated() - _ = await_updated() - - alerts = Agent.get(fake_store, & &1) - assert alerts == [] - end - - defp await_updated do - receive do - :updated -> :ok - after - 100 -> :no_msg - end - end -end diff --git a/test/screenplay/alerts/cache/store_test.exs b/test/screenplay/alerts/cache/store_test.exs deleted file mode 100644 index 847b3983..00000000 --- a/test/screenplay/alerts/cache/store_test.exs +++ /dev/null @@ -1,41 +0,0 @@ -defmodule Screenplay.Alerts.Cache.StoreTest do - use ExUnit.Case - - alias Screenplay.Alerts.Alert - alias Screenplay.Alerts.Cache.Store - - setup_all do - _ = start_supervised(Store) - :ok - end - - test "updating and fetching" do - alert1 = - struct(Alert, - id: "123", - informed_entities: [ - %{route: "Blue"}, - %{stop: "place-pktrm"} - ] - ) - - alert2 = - struct(Alert, - id: "456", - informed_entities: [ - %{route: "Red"} - ] - ) - - alerts = [alert1, alert2] - - Store.update(alerts) - - expected_alerts = MapSet.new(alerts) - actual_alerts = MapSet.new(Store.alerts()) - - assert expected_alerts == actual_alerts - assert Store.alert("123") == alert1 - assert Store.alert("aaa") == nil - end -end diff --git a/test/screenplay/alerts/cache_test.exs b/test/screenplay/alerts/cache_test.exs new file mode 100644 index 00000000..dcc1eff9 --- /dev/null +++ b/test/screenplay/alerts/cache_test.exs @@ -0,0 +1,64 @@ +defmodule Screenplay.Alerts.CacheTest do + use ExUnit.Case + + alias Screenplay.Alerts.{Alert, Cache} + + defp alert_json(id) do + %{ + "id" => id, + "attributes" => %{ + "active_period" => [], + "created_at" => nil, + "updated_at" => nil, + "cause" => nil, + "effect" => nil, + "header" => nil, + "informed_entity" => [], + "lifecycle" => nil, + "severity" => nil, + "timeframe" => nil, + "url" => nil, + "description" => nil + } + } + end + + test "It polls an API and updates the store" do + get_json_fn = fn "alerts", %{"include" => "routes"} -> + {:ok, %{"data" => [alert_json("1")], "included" => []}} + end + + {:ok, fetcher} = + start_supervised({Cache, get_json_fn: get_json_fn, update_interval_ms: 10_000}) + + send(fetcher, :fetch) + _ = await_updated() + + assert [%Alert{id: "1"}] = Cache.alerts() + assert %Alert{id: "1"} = Cache.alert("1") + end + + test "It handles a failed API response and does not update the store" do + get_json_fn = fn "alerts", %{"include" => "routes"} -> + :error + end + + {:ok, fetcher} = + start_supervised({Cache, get_json_fn: get_json_fn, update_interval_ms: 10_000}) + + send(fetcher, :fetch) + _ = await_updated() + + alerts = Cache.alerts() + assert alerts == [] + refute Cache.alert("1") + end + + defp await_updated do + receive do + :updated -> :ok + after + 100 -> :no_msg + end + end +end From 32af27e8e9fa70283b030123b275c081b922ed94 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Wed, 10 Apr 2024 13:39:23 -0400 Subject: [PATCH 09/38] Removed unnecessary pattern match. --- lib/screenplay/alerts/cache.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screenplay/alerts/cache.ex b/lib/screenplay/alerts/cache.ex index 33e600cc..6393f953 100644 --- a/lib/screenplay/alerts/cache.ex +++ b/lib/screenplay/alerts/cache.ex @@ -43,7 +43,7 @@ defmodule Screenplay.Alerts.Cache do # Server @impl true def init(opts) do - _ = :ets.new(:alerts, [:protected, :named_table, read_concurrency: true]) + :ets.new(:alerts, [:protected, :named_table, read_concurrency: true]) get_json_fn = Keyword.get(opts, :get_json_fn) update_interval_ms = Keyword.get(opts, :update_interval_ms) From c49da3450ed2bb0e3439e17a2262ec9e2fb19d48 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Wed, 10 Apr 2024 13:41:09 -0400 Subject: [PATCH 10/38] Changed reduce to map. --- lib/screenplay/alerts/cache.ex | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/screenplay/alerts/cache.ex b/lib/screenplay/alerts/cache.ex index 6393f953..f4db65dc 100644 --- a/lib/screenplay/alerts/cache.ex +++ b/lib/screenplay/alerts/cache.ex @@ -57,10 +57,7 @@ defmodule Screenplay.Alerts.Cache do def handle_info(:fetch, state = {get_json_fn, update_interval_ms}) do case Alert.fetch(get_json_fn) do {:ok, alerts} -> - alerts_to_insert = - Enum.reduce(alerts, [], fn alert, acc -> - [{alert.id, alert} | acc] - end) + alerts_to_insert = Enum.map(alerts, fn alert -> {alert.id, alert} end) :ets.delete_all_objects(:alerts) :ets.insert(:alerts, alerts_to_insert) From 142c2d1c928169f02c7edf1e226e32e6ac47d63a Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Mon, 1 Apr 2024 10:41:12 -0400 Subject: [PATCH 11/38] Fixed AuthManager so users can have more than one role. --- .../auth_manager/auth_manager.ex | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/screenplay_web/auth_manager/auth_manager.ex b/lib/screenplay_web/auth_manager/auth_manager.ex index 0341935e..202cecc4 100644 --- a/lib/screenplay_web/auth_manager/auth_manager.ex +++ b/lib/screenplay_web/auth_manager/auth_manager.ex @@ -3,7 +3,7 @@ defmodule ScreenplayWeb.AuthManager do use Guardian, otp_app: :screenplay - @type access_level :: :none | :read_only | :emergency_admin | :screens_config_admmin + @type access_level :: :none | :read_only | :emergency_admin | :screens_config_admin @screenplay_admin_role "screenplay-emergency-admin" @screens_admin "screens-admin" @@ -24,21 +24,25 @@ defmodule ScreenplayWeb.AuthManager do def resource_from_claims(_), do: {:error, :invalid_claims} - @spec claims_access_level(Guardian.Token.claims()) :: access_level() + @spec claims_access_level(Guardian.Token.claims()) :: list(access_level()) def claims_access_level(%{"roles" => roles}) when not is_nil(roles) do - cond do - @screenplay_admin_role in roles -> - :emergency_admin - - @screens_admin in roles -> - :screens_config_admmin - - true -> - :read_only + access_levels = + [] + |> append_if(@screenplay_admin_role in roles, :emergency_admin) + |> append_if(@screens_admin in roles, :screens_admin) + + if access_levels == [] do + [:read_only] + else + access_levels end end def claims_access_level(_claims) do - :read_only + [:read_only] + end + + defp append_if(list, condition, item) do + if condition, do: list ++ [item], else: list end end From a726335945e6e470b1d78915964bd5b8eed7b88a Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Mon, 1 Apr 2024 10:41:51 -0400 Subject: [PATCH 12/38] Changed attribute name to be more specific. --- lib/screenplay_web/auth_manager/auth_manager.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screenplay_web/auth_manager/auth_manager.ex b/lib/screenplay_web/auth_manager/auth_manager.ex index 202cecc4..6cc5bce6 100644 --- a/lib/screenplay_web/auth_manager/auth_manager.ex +++ b/lib/screenplay_web/auth_manager/auth_manager.ex @@ -5,7 +5,7 @@ defmodule ScreenplayWeb.AuthManager do @type access_level :: :none | :read_only | :emergency_admin | :screens_config_admin - @screenplay_admin_role "screenplay-emergency-admin" + @screenplay_emergency_admin_role "screenplay-emergency-admin" @screens_admin "screens-admin" @spec subject_for_token( @@ -28,7 +28,7 @@ defmodule ScreenplayWeb.AuthManager do def claims_access_level(%{"roles" => roles}) when not is_nil(roles) do access_levels = [] - |> append_if(@screenplay_admin_role in roles, :emergency_admin) + |> append_if(@screenplay_emergency_admin_role in roles, :emergency_admin) |> append_if(@screens_admin in roles, :screens_admin) if access_levels == [] do From 9b8ab68658754965606609901767d5999158b901 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Mon, 1 Apr 2024 13:16:35 -0400 Subject: [PATCH 13/38] Added new role for PA Message workflow. --- lib/screenplay_web/auth_manager/auth_manager.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/screenplay_web/auth_manager/auth_manager.ex b/lib/screenplay_web/auth_manager/auth_manager.ex index 6cc5bce6..db745fbe 100644 --- a/lib/screenplay_web/auth_manager/auth_manager.ex +++ b/lib/screenplay_web/auth_manager/auth_manager.ex @@ -3,10 +3,12 @@ defmodule ScreenplayWeb.AuthManager do use Guardian, otp_app: :screenplay - @type access_level :: :none | :read_only | :emergency_admin | :screens_config_admin + @type access_level :: + :none | :read_only | :emergency_admin | :screens_config_admin | :pa_message_admin @screenplay_emergency_admin_role "screenplay-emergency-admin" @screens_admin "screens-admin" + @pa_message_admin "pa-message-admin" @spec subject_for_token( resource :: Guardian.Token.resource(), @@ -30,6 +32,7 @@ defmodule ScreenplayWeb.AuthManager do [] |> append_if(@screenplay_emergency_admin_role in roles, :emergency_admin) |> append_if(@screens_admin in roles, :screens_admin) + |> append_if(@pa_message_admin in roles, :pa_message_admin) if access_levels == [] do [:read_only] From 33e058a48fb19f608a4afc19cb2304764c3562ae Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Mon, 1 Apr 2024 13:22:21 -0400 Subject: [PATCH 14/38] Renamed a role so it better represents how it's used. --- .../js/components/Dashboard/ScreenDetailActionBar.tsx | 10 ++++++---- assets/tests/components/reportAProblemButton.test.tsx | 2 +- .../auth_manager/ensure_screenplay_admin_group.ex | 2 +- lib/screenplay_web/plugs/metadata.ex | 7 +++---- lib/screenplay_web/templates/layout/app.html.heex | 4 ++-- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/assets/js/components/Dashboard/ScreenDetailActionBar.tsx b/assets/js/components/Dashboard/ScreenDetailActionBar.tsx index f0e5b4a6..c598cc2c 100644 --- a/assets/js/components/Dashboard/ScreenDetailActionBar.tsx +++ b/assets/js/components/Dashboard/ScreenDetailActionBar.tsx @@ -81,9 +81,11 @@ const ScreenDetailActionBar = ( ); }; - const isAdmin = document.querySelector("meta[name=is-admin]"); + const isEmergencyAdmin = document.querySelector( + "meta[name=is-emergency-admin]" + ); - const adminLink = isAdmin + const reportAProblemURL = isEmergencyAdmin ? "https://mbta.slack.com/channels/screens-team-pios" : "https://mbta.slack.com/channels/screens"; @@ -120,7 +122,7 @@ const ScreenDetailActionBar = ( e.stopPropagation()} target="_blank" > @@ -138,7 +140,7 @@ const ScreenDetailActionBar = ( url={props.screenUrl} queueToastExpiration={queueToastExpiration} /> - + ); } diff --git a/assets/tests/components/reportAProblemButton.test.tsx b/assets/tests/components/reportAProblemButton.test.tsx index 9c1a5e9e..4bc8f9fd 100644 --- a/assets/tests/components/reportAProblemButton.test.tsx +++ b/assets/tests/components/reportAProblemButton.test.tsx @@ -18,7 +18,7 @@ describe("ReportAProblemButton", () => { test("uses correct URL for admins", async () => { const meta = document.createElement("meta"); - meta.setAttribute("name", "is-admin"); + meta.setAttribute("name", "is-emergency-admin"); document.head.appendChild(meta); const { getByTestId } = render( diff --git a/lib/screenplay_web/auth_manager/ensure_screenplay_admin_group.ex b/lib/screenplay_web/auth_manager/ensure_screenplay_admin_group.ex index cf5a34a0..635dc530 100644 --- a/lib/screenplay_web/auth_manager/ensure_screenplay_admin_group.ex +++ b/lib/screenplay_web/auth_manager/ensure_screenplay_admin_group.ex @@ -11,7 +11,7 @@ defmodule ScreenplayWeb.EnsureScreenplayAdminGroup do def call(conn, _opts) do with claims <- Guardian.Plug.current_claims(conn), - true <- ScreenplayWeb.AuthManager.claims_access_level(claims) == :emergency_admin do + true <- :emergency_admin in ScreenplayWeb.AuthManager.claims_access_level(claims) do conn else _ -> diff --git a/lib/screenplay_web/plugs/metadata.ex b/lib/screenplay_web/plugs/metadata.ex index 5ea61ccc..a706966d 100644 --- a/lib/screenplay_web/plugs/metadata.ex +++ b/lib/screenplay_web/plugs/metadata.ex @@ -32,14 +32,13 @@ defmodule ScreenplayWeb.Plugs.Metadata do |> assign(:alerts_ui_url, Application.get_env(:screenplay, :alerts_ui_url)) |> assign(:screens_url, Application.get_env(:screenplay, :screens_url)) |> assign(:signs_ui_url, Application.get_env(:screenplay, :signs_ui_url)) - |> assign(:is_admin, admin?(conn)) + |> assign(:is_emergency_admin, emergency_admin?(conn)) |> assign(:fullstory_org_id, Application.get_env(:screenplay, :fullstory_org_id)) end - defp admin?(conn) do + defp emergency_admin?(conn) do claims = Guardian.Plug.current_claims(conn) - not is_nil(claims) and - ScreenplayWeb.AuthManager.claims_access_level(claims) == :emergency_admin + :emergency_admin in ScreenplayWeb.AuthManager.claims_access_level(claims) end end diff --git a/lib/screenplay_web/templates/layout/app.html.heex b/lib/screenplay_web/templates/layout/app.html.heex index 2227940e..fa84ff06 100644 --- a/lib/screenplay_web/templates/layout/app.html.heex +++ b/lib/screenplay_web/templates/layout/app.html.heex @@ -20,8 +20,8 @@ <%= if @alerts_ui_url do %> <% end %> - <%= if @is_admin do %> - + <%= if @is_emergency_admin do %> + <% end %> <%= if @fullstory_org_id do %> From 12d18a9ee7e67bd100b9453add00b2dc0bd313ef Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Mon, 1 Apr 2024 13:30:59 -0400 Subject: [PATCH 15/38] Added a plug for verifying permission. --- .../auth_manager/ensure_pa_message_admin.ex | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 lib/screenplay_web/auth_manager/ensure_pa_message_admin.ex diff --git a/lib/screenplay_web/auth_manager/ensure_pa_message_admin.ex b/lib/screenplay_web/auth_manager/ensure_pa_message_admin.ex new file mode 100644 index 00000000..367b6a7e --- /dev/null +++ b/lib/screenplay_web/auth_manager/ensure_pa_message_admin.ex @@ -0,0 +1,22 @@ +defmodule ScreenplayWeb.EnsurePaMessageAdmin do + @moduledoc """ + Verify that the user has permission to access the PA Message Creation feature. + """ + + import Plug.Conn, only: [halt: 1] + import Phoenix.Controller, only: [redirect: 2] + + def init(options), do: options + + def call(conn, _opts) do + with claims <- Guardian.Plug.current_claims(conn), + true <- :pa_message_admin in ScreenplayWeb.AuthManager.claims_access_level(claims) do + conn + else + _ -> + conn + |> redirect(to: "/dashboard") + |> halt() + end + end +end From 330bbdc02e68cc9c1d51c662abda0fee072bbeab Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Mon, 1 Apr 2024 13:31:24 -0400 Subject: [PATCH 16/38] Added scope with an auth check for PA message creation. --- lib/screenplay_web/router.ex | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/screenplay_web/router.ex b/lib/screenplay_web/router.ex index 1d79e408..97233bac 100644 --- a/lib/screenplay_web/router.ex +++ b/lib/screenplay_web/router.ex @@ -35,6 +35,10 @@ defmodule ScreenplayWeb.Router do plug(ScreenplayWeb.EnsureScreenplayAdminGroup) end + pipeline :ensure_pa_message_admin do + plug(ScreenplayWeb.EnsurePaMessageAdmin) + end + # Load balancer health check # Exempt from auth checks and SSL redirects scope "/", ScreenplayWeb do @@ -63,6 +67,17 @@ defmodule ScreenplayWeb.Router do get("/unauthorized", UnauthorizedController, :index) end + scope "/", ScreenplayWeb do + pipe_through([ + :redirect_prod_http, + :browser, + :auth, + :ensure_auth, + :ensure_pa_message_admin, + :metadata + ]) + end + scope "/api", ScreenplayWeb do pipe_through([:redirect_prod_http, :browser, :auth, :ensure_auth]) From 33e1a4066bd0bef5d60b0f5bedd906d5159b18a9 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Mon, 1 Apr 2024 13:33:56 -0400 Subject: [PATCH 17/38] Added role to fake Keycloak config. --- config/dev.exs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/dev.exs b/config/dev.exs index 68b79715..9628cda8 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -85,7 +85,9 @@ config :screenplay, config :ueberauth, Ueberauth, providers: [ - keycloak: {Screenplay.Ueberauth.Strategy.Fake, [roles: ["screenplay-emergency-admin"]]} + keycloak: + {Screenplay.Ueberauth.Strategy.Fake, + [roles: ["screenplay-emergency-admin", "pa-message-admin"]]} ] config :ueberauth_oidcc, From 9873488101a03634377e5aa63d0561adeb3bd52f Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Mon, 1 Apr 2024 13:38:58 -0400 Subject: [PATCH 18/38] Added new role to metadata. --- lib/screenplay_web/plugs/metadata.ex | 7 +++++++ lib/screenplay_web/templates/layout/app.html.heex | 3 +++ 2 files changed, 10 insertions(+) diff --git a/lib/screenplay_web/plugs/metadata.ex b/lib/screenplay_web/plugs/metadata.ex index a706966d..c5e4e36f 100644 --- a/lib/screenplay_web/plugs/metadata.ex +++ b/lib/screenplay_web/plugs/metadata.ex @@ -33,6 +33,7 @@ defmodule ScreenplayWeb.Plugs.Metadata do |> assign(:screens_url, Application.get_env(:screenplay, :screens_url)) |> assign(:signs_ui_url, Application.get_env(:screenplay, :signs_ui_url)) |> assign(:is_emergency_admin, emergency_admin?(conn)) + |> assign(:is_pa_message_admin, pa_message_admin?(conn)) |> assign(:fullstory_org_id, Application.get_env(:screenplay, :fullstory_org_id)) end @@ -41,4 +42,10 @@ defmodule ScreenplayWeb.Plugs.Metadata do :emergency_admin in ScreenplayWeb.AuthManager.claims_access_level(claims) end + + defp pa_message_admin?(conn) do + claims = Guardian.Plug.current_claims(conn) + + :pa_message_admin in ScreenplayWeb.AuthManager.claims_access_level(claims) + end end diff --git a/lib/screenplay_web/templates/layout/app.html.heex b/lib/screenplay_web/templates/layout/app.html.heex index fa84ff06..d897a762 100644 --- a/lib/screenplay_web/templates/layout/app.html.heex +++ b/lib/screenplay_web/templates/layout/app.html.heex @@ -23,6 +23,9 @@ <%= if @is_emergency_admin do %> <% end %> + <%= if @is_pa_message_admin do %> + + <% end %> <%= if @fullstory_org_id do %> <% end %> From bcd0142542578a0df93430e4a32e7dad2982bf55 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Tue, 2 Apr 2024 11:03:07 -0400 Subject: [PATCH 19/38] Renamed modules and files to better describe functionality. --- ...p.ex => ensure_screenplay_emergency_admin_group.ex} | 2 +- lib/screenplay_web/router.ex | 8 ++++---- ...> ensure_screenplay_emergency_admin_group_test.exs} | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) rename lib/screenplay_web/auth_manager/{ensure_screenplay_admin_group.ex => ensure_screenplay_emergency_admin_group.ex} (89%) rename test/screenplay_web/{ensure_screenplay_admin_group_test.exs => ensure_screenplay_emergency_admin_group_test.exs} (55%) diff --git a/lib/screenplay_web/auth_manager/ensure_screenplay_admin_group.ex b/lib/screenplay_web/auth_manager/ensure_screenplay_emergency_admin_group.ex similarity index 89% rename from lib/screenplay_web/auth_manager/ensure_screenplay_admin_group.ex rename to lib/screenplay_web/auth_manager/ensure_screenplay_emergency_admin_group.ex index 635dc530..d57d55e9 100644 --- a/lib/screenplay_web/auth_manager/ensure_screenplay_admin_group.ex +++ b/lib/screenplay_web/auth_manager/ensure_screenplay_emergency_admin_group.ex @@ -1,4 +1,4 @@ -defmodule ScreenplayWeb.EnsureScreenplayAdminGroup do +defmodule ScreenplayWeb.EnsureScreenplayEmergencyAdminGroup do @moduledoc """ Verify that the user has permission to access the Outfront Takeover Tool. """ diff --git a/lib/screenplay_web/router.ex b/lib/screenplay_web/router.ex index 97233bac..15b0b932 100644 --- a/lib/screenplay_web/router.ex +++ b/lib/screenplay_web/router.ex @@ -31,8 +31,8 @@ defmodule ScreenplayWeb.Router do plug(Guardian.Plug.EnsureAuthenticated) end - pipeline :ensure_screenplay_admin_group do - plug(ScreenplayWeb.EnsureScreenplayAdminGroup) + pipeline :ensure_screenplay_emergency_admin_group do + plug(ScreenplayWeb.EnsureScreenplayEmergencyAdminGroup) end pipeline :ensure_pa_message_admin do @@ -51,7 +51,7 @@ defmodule ScreenplayWeb.Router do :browser, :auth, :ensure_auth, - :ensure_screenplay_admin_group, + :ensure_screenplay_emergency_admin_group, :metadata ]) @@ -99,7 +99,7 @@ defmodule ScreenplayWeb.Router do :browser, :auth, :ensure_auth, - :ensure_screenplay_admin_group + :ensure_screenplay_emergency_admin_group ]) post("/create", AlertController, :create) diff --git a/test/screenplay_web/ensure_screenplay_admin_group_test.exs b/test/screenplay_web/ensure_screenplay_emergency_admin_group_test.exs similarity index 55% rename from test/screenplay_web/ensure_screenplay_admin_group_test.exs rename to test/screenplay_web/ensure_screenplay_emergency_admin_group_test.exs index 16d5e616..26907941 100644 --- a/test/screenplay_web/ensure_screenplay_admin_group_test.exs +++ b/test/screenplay_web/ensure_screenplay_emergency_admin_group_test.exs @@ -1,21 +1,21 @@ -defmodule ScreenplayWeb.EnsureScreenplayAdminGroupTest do +defmodule ScreenplayWeb.EnsureScreenplayEmergencyAdminGroupTest do use ScreenplayWeb.ConnCase describe "init/1" do test "passes options through unchanged" do - assert ScreenplayWeb.EnsureScreenplayAdminGroup.init([]) == [] + assert ScreenplayWeb.EnsureScreenplayEmergencyAdminGroup.init([]) == [] end end describe "call/2" do - @tag :authenticated_admin + @tag :authenticated_emergency_admin test "does nothing when user is in the outfront admin group", %{conn: conn} do - assert conn == ScreenplayWeb.EnsureScreenplayAdminGroup.call(conn, []) + assert conn == ScreenplayWeb.EnsureScreenplayEmergencyAdminGroup.call(conn, []) end @tag :authenticated test "redirects when user is not in the outfront admin group", %{conn: conn} do - conn = ScreenplayWeb.EnsureScreenplayAdminGroup.call(conn, []) + conn = ScreenplayWeb.EnsureScreenplayEmergencyAdminGroup.call(conn, []) response = html_response(conn, 302) assert response =~ "/unauthorized" From c226b5ba388591eee3445a253621a8da40bdd950 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Tue, 2 Apr 2024 11:03:25 -0400 Subject: [PATCH 20/38] Changed references of admin to emergency admin. --- assets/tests/components/reportAProblemButton.test.tsx | 4 ++-- test/screenplay_web/controllers/alerts_controller_test.exs | 2 +- .../controllers/dashboard_api_controller_test.exs | 2 +- test/screenplay_web/controllers/dashboard_controller_test.exs | 2 +- .../outfront_takeover_tool/page_controller_test.exs | 4 ++-- test/support/conn_case.ex | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/assets/tests/components/reportAProblemButton.test.tsx b/assets/tests/components/reportAProblemButton.test.tsx index 4bc8f9fd..fe48e871 100644 --- a/assets/tests/components/reportAProblemButton.test.tsx +++ b/assets/tests/components/reportAProblemButton.test.tsx @@ -3,7 +3,7 @@ import { render, waitFor } from "@testing-library/react"; import ReportAProblemButton from "../../js/components/Dashboard/ReportAProblemButton"; describe("ReportAProblemButton", () => { - test("uses correct URL for non-admin users", async () => { + test("uses correct URL for users that are not emergency admins", async () => { const { getByTestId } = render( ); @@ -16,7 +16,7 @@ describe("ReportAProblemButton", () => { ); }); - test("uses correct URL for admins", async () => { + test("uses correct URL for emergency admins", async () => { const meta = document.createElement("meta"); meta.setAttribute("name", "is-emergency-admin"); document.head.appendChild(meta); diff --git a/test/screenplay_web/controllers/alerts_controller_test.exs b/test/screenplay_web/controllers/alerts_controller_test.exs index d7ab8276..9705a2d4 100644 --- a/test/screenplay_web/controllers/alerts_controller_test.exs +++ b/test/screenplay_web/controllers/alerts_controller_test.exs @@ -2,7 +2,7 @@ defmodule ScreenplayWeb.Controllers.AlertsControllerTest do use ScreenplayWeb.ConnCase describe "index/2" do - @tag :authenticated_admin + @tag :authenticated_emergency_admin test "responds 200 to authenticated admin requests", %{conn: conn} do conn = get(conn, "/alerts") assert %{status: 200} = conn diff --git a/test/screenplay_web/controllers/dashboard_api_controller_test.exs b/test/screenplay_web/controllers/dashboard_api_controller_test.exs index 240e32e5..96ae34aa 100644 --- a/test/screenplay_web/controllers/dashboard_api_controller_test.exs +++ b/test/screenplay_web/controllers/dashboard_api_controller_test.exs @@ -2,7 +2,7 @@ defmodule ScreenplayWeb.Controllers.DashboardApiControllerTest do use ScreenplayWeb.ConnCase describe "index/2" do - @tag :authenticated_admin + @tag :authenticated_emergency_admin test "responds 200 to authenticated admin requests", %{conn: conn} do conn = get(conn, "/dashboard") assert %{status: 200} = conn diff --git a/test/screenplay_web/controllers/dashboard_controller_test.exs b/test/screenplay_web/controllers/dashboard_controller_test.exs index 3bb117eb..dc1d987c 100644 --- a/test/screenplay_web/controllers/dashboard_controller_test.exs +++ b/test/screenplay_web/controllers/dashboard_controller_test.exs @@ -2,7 +2,7 @@ defmodule ScreenplayWeb.Controllers.DashboardControllerTest do use ScreenplayWeb.ConnCase describe "index/2" do - @tag :authenticated_admin + @tag :authenticated_emergency_admin test "responds 200 to authenticated admin requests", %{conn: conn} do conn = get(conn, "/dashboard") assert %{status: 200} = conn diff --git a/test/screenplay_web/controllers/outfront_takeover_tool/page_controller_test.exs b/test/screenplay_web/controllers/outfront_takeover_tool/page_controller_test.exs index 71fd5dfc..f9b6e65d 100644 --- a/test/screenplay_web/controllers/outfront_takeover_tool/page_controller_test.exs +++ b/test/screenplay_web/controllers/outfront_takeover_tool/page_controller_test.exs @@ -2,7 +2,7 @@ defmodule ScreenplayWeb.OutfrontTakeoverTool.PageControllerTest do use ScreenplayWeb.ConnCase describe "index/2" do - @tag :authenticated_admin + @tag :authenticated_emergency_admin test "responds 200 to authenticated admin requests", %{conn: conn} do conn = get(conn, "/emergency-takeover") assert %{status: 200} = conn @@ -21,7 +21,7 @@ defmodule ScreenplayWeb.OutfrontTakeoverTool.PageControllerTest do end describe "takeover_redirect/2" do - @tag :authenticated_admin + @tag :authenticated_emergency_admin test "redirects admin to /emergency-takeover", %{conn: conn} do conn = get(conn, "/") assert redirected_to(conn) =~ "/emergency-takeover" diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 6d04c893..33ca5e79 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -34,7 +34,7 @@ defmodule ScreenplayWeb.ConnCase do setup tags do {conn, user} = cond do - tags[:authenticated_admin] -> + tags[:authenticated_emergency_admin] -> user = "test_user" conn = From c2f686bd037c010f685e7bac62dc8fd7bf32808a Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Tue, 2 Apr 2024 11:07:19 -0400 Subject: [PATCH 21/38] Simplified how roles are mapped to access levels. --- .../auth_manager/auth_manager.ex | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/screenplay_web/auth_manager/auth_manager.ex b/lib/screenplay_web/auth_manager/auth_manager.ex index db745fbe..1fa8d386 100644 --- a/lib/screenplay_web/auth_manager/auth_manager.ex +++ b/lib/screenplay_web/auth_manager/auth_manager.ex @@ -6,9 +6,11 @@ defmodule ScreenplayWeb.AuthManager do @type access_level :: :none | :read_only | :emergency_admin | :screens_config_admin | :pa_message_admin - @screenplay_emergency_admin_role "screenplay-emergency-admin" - @screens_admin "screens-admin" - @pa_message_admin "pa-message-admin" + @roles %{ + "screenplay-emergency-admin" => :emergency_admin, + "screens-admin" => :screens_admin, + "pa-message-admin" => :pa_message_admin + } @spec subject_for_token( resource :: Guardian.Token.resource(), @@ -28,11 +30,7 @@ defmodule ScreenplayWeb.AuthManager do @spec claims_access_level(Guardian.Token.claims()) :: list(access_level()) def claims_access_level(%{"roles" => roles}) when not is_nil(roles) do - access_levels = - [] - |> append_if(@screenplay_emergency_admin_role in roles, :emergency_admin) - |> append_if(@screens_admin in roles, :screens_admin) - |> append_if(@pa_message_admin in roles, :pa_message_admin) + access_levels = Enum.map(roles, &Map.get(@roles, &1)) |> Enum.reject(&is_nil/1) if access_levels == [] do [:read_only] @@ -44,8 +42,4 @@ defmodule ScreenplayWeb.AuthManager do def claims_access_level(_claims) do [:read_only] end - - defp append_if(list, condition, item) do - if condition, do: list ++ [item], else: list - end end From 5e80d90dc5be1b7a602f8f9b7bd71ccd76343dbb Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Tue, 2 Apr 2024 11:10:47 -0400 Subject: [PATCH 22/38] Removed explicit readonly permission. Can be implied. --- lib/screenplay_web/auth_manager/auth_manager.ex | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/screenplay_web/auth_manager/auth_manager.ex b/lib/screenplay_web/auth_manager/auth_manager.ex index 1fa8d386..b233d7a6 100644 --- a/lib/screenplay_web/auth_manager/auth_manager.ex +++ b/lib/screenplay_web/auth_manager/auth_manager.ex @@ -3,8 +3,7 @@ defmodule ScreenplayWeb.AuthManager do use Guardian, otp_app: :screenplay - @type access_level :: - :none | :read_only | :emergency_admin | :screens_config_admin | :pa_message_admin + @type access_level :: :emergency_admin | :screens_config_admin | :pa_message_admin @roles %{ "screenplay-emergency-admin" => :emergency_admin, @@ -30,16 +29,10 @@ defmodule ScreenplayWeb.AuthManager do @spec claims_access_level(Guardian.Token.claims()) :: list(access_level()) def claims_access_level(%{"roles" => roles}) when not is_nil(roles) do - access_levels = Enum.map(roles, &Map.get(@roles, &1)) |> Enum.reject(&is_nil/1) - - if access_levels == [] do - [:read_only] - else - access_levels - end + Enum.map(roles, &Map.get(@roles, &1)) |> Enum.reject(&is_nil/1) end def claims_access_level(_claims) do - [:read_only] + [] end end From 8842d8ba69a375eb8632e33498446f74f5cced90 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Tue, 2 Apr 2024 11:29:53 -0400 Subject: [PATCH 23/38] Added roles list to metadata to cut down on function calls. --- .../auth_manager/ensure_pa_message_admin.ex | 12 +++++------- .../ensure_screenplay_emergency_admin_group.ex | 12 +++++------- lib/screenplay_web/plugs/metadata.ex | 17 +++-------------- lib/screenplay_web/router.ex | 9 +++++---- .../templates/layout/app.html.heex | 8 ++++---- 5 files changed, 22 insertions(+), 36 deletions(-) diff --git a/lib/screenplay_web/auth_manager/ensure_pa_message_admin.ex b/lib/screenplay_web/auth_manager/ensure_pa_message_admin.ex index 367b6a7e..c5bace46 100644 --- a/lib/screenplay_web/auth_manager/ensure_pa_message_admin.ex +++ b/lib/screenplay_web/auth_manager/ensure_pa_message_admin.ex @@ -8,15 +8,13 @@ defmodule ScreenplayWeb.EnsurePaMessageAdmin do def init(options), do: options - def call(conn, _opts) do - with claims <- Guardian.Plug.current_claims(conn), - true <- :pa_message_admin in ScreenplayWeb.AuthManager.claims_access_level(claims) do + def call(conn = %{assigns: %{roles: roles}}, _opts) do + if :pa_message_admin in roles do conn else - _ -> - conn - |> redirect(to: "/dashboard") - |> halt() + conn + |> redirect(to: "/dashboard") + |> halt() end end end diff --git a/lib/screenplay_web/auth_manager/ensure_screenplay_emergency_admin_group.ex b/lib/screenplay_web/auth_manager/ensure_screenplay_emergency_admin_group.ex index d57d55e9..8243b797 100644 --- a/lib/screenplay_web/auth_manager/ensure_screenplay_emergency_admin_group.ex +++ b/lib/screenplay_web/auth_manager/ensure_screenplay_emergency_admin_group.ex @@ -9,15 +9,13 @@ defmodule ScreenplayWeb.EnsureScreenplayEmergencyAdminGroup do def init(options), do: options - def call(conn, _opts) do - with claims <- Guardian.Plug.current_claims(conn), - true <- :emergency_admin in ScreenplayWeb.AuthManager.claims_access_level(claims) do + def call(conn = %{assigns: %{roles: roles}}, _opts) do + if :emergency_admin in roles do conn else - _ -> - conn - |> Phoenix.Controller.redirect(to: Helpers.unauthorized_path(conn, :index)) - |> halt() + conn + |> Phoenix.Controller.redirect(to: Helpers.unauthorized_path(conn, :index)) + |> halt() end end end diff --git a/lib/screenplay_web/plugs/metadata.ex b/lib/screenplay_web/plugs/metadata.ex index c5e4e36f..442cf297 100644 --- a/lib/screenplay_web/plugs/metadata.ex +++ b/lib/screenplay_web/plugs/metadata.ex @@ -25,6 +25,8 @@ defmodule ScreenplayWeb.Plugs.Metadata do "" end + claims = Guardian.Plug.current_claims(conn) + conn |> assign(:username, username) |> assign(:environment_name, Application.get_env(:screenplay, :environment_name, "dev")) @@ -32,20 +34,7 @@ defmodule ScreenplayWeb.Plugs.Metadata do |> assign(:alerts_ui_url, Application.get_env(:screenplay, :alerts_ui_url)) |> assign(:screens_url, Application.get_env(:screenplay, :screens_url)) |> assign(:signs_ui_url, Application.get_env(:screenplay, :signs_ui_url)) - |> assign(:is_emergency_admin, emergency_admin?(conn)) - |> assign(:is_pa_message_admin, pa_message_admin?(conn)) + |> assign(:roles, ScreenplayWeb.AuthManager.claims_access_level(claims)) |> assign(:fullstory_org_id, Application.get_env(:screenplay, :fullstory_org_id)) end - - defp emergency_admin?(conn) do - claims = Guardian.Plug.current_claims(conn) - - :emergency_admin in ScreenplayWeb.AuthManager.claims_access_level(claims) - end - - defp pa_message_admin?(conn) do - claims = Guardian.Plug.current_claims(conn) - - :pa_message_admin in ScreenplayWeb.AuthManager.claims_access_level(claims) - end end diff --git a/lib/screenplay_web/router.ex b/lib/screenplay_web/router.ex index 15b0b932..744fd41c 100644 --- a/lib/screenplay_web/router.ex +++ b/lib/screenplay_web/router.ex @@ -51,8 +51,8 @@ defmodule ScreenplayWeb.Router do :browser, :auth, :ensure_auth, - :ensure_screenplay_emergency_admin_group, - :metadata + :metadata, + :ensure_screenplay_emergency_admin_group ]) get("/", PageController, :takeover_redirect) @@ -73,8 +73,8 @@ defmodule ScreenplayWeb.Router do :browser, :auth, :ensure_auth, - :ensure_pa_message_admin, - :metadata + :metadata, + :ensure_pa_message_admin ]) end @@ -99,6 +99,7 @@ defmodule ScreenplayWeb.Router do :browser, :auth, :ensure_auth, + :metadata, :ensure_screenplay_emergency_admin_group ]) diff --git a/lib/screenplay_web/templates/layout/app.html.heex b/lib/screenplay_web/templates/layout/app.html.heex index d897a762..dec75d51 100644 --- a/lib/screenplay_web/templates/layout/app.html.heex +++ b/lib/screenplay_web/templates/layout/app.html.heex @@ -20,11 +20,11 @@ <%= if @alerts_ui_url do %> <% end %> - <%= if @is_emergency_admin do %> - + <%= if :emergency_admin in @roles do %> + <% end %> - <%= if @is_pa_message_admin do %> - + <%= if :pa_message_admin in @roles do %> + <% end %> <%= if @fullstory_org_id do %> From b552e83c8d445e244467b169ea0e4bdaeba560a4 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Tue, 2 Apr 2024 12:05:58 -0400 Subject: [PATCH 24/38] Fixed tests now that roles are in assigns. --- test/support/conn_case.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 33ca5e79..524660d5 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -13,6 +13,7 @@ defmodule ScreenplayWeb.ConnCase do this option is not recommended for other databases. """ + import Plug.Conn use ExUnit.CaseTemplate using do @@ -44,6 +45,7 @@ defmodule ScreenplayWeb.ConnCase do "roles" => ["screenplay-emergency-admin"] }) |> Plug.Conn.put_session(:username, user) + |> assign(:roles, [:emergency_admin]) {conn, user} @@ -55,6 +57,7 @@ defmodule ScreenplayWeb.ConnCase do |> Plug.Test.init_test_session(%{}) |> Guardian.Plug.sign_in(ScreenplayWeb.AuthManager, user, %{roles: []}) |> Plug.Conn.put_session(:username, user) + |> assign(:roles, []) {conn, user} From 196435307c8ee2aff573507d48f00f906cad32ed Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Tue, 2 Apr 2024 12:06:34 -0400 Subject: [PATCH 25/38] Added test for new plug. --- .../ensure_pa_message_admin_test.exs | 23 +++++++++++++++++++ test/support/conn_case.ex | 14 +++++++++++ 2 files changed, 37 insertions(+) create mode 100644 test/screenplay_web/ensure_pa_message_admin_test.exs diff --git a/test/screenplay_web/ensure_pa_message_admin_test.exs b/test/screenplay_web/ensure_pa_message_admin_test.exs new file mode 100644 index 00000000..6f17b28e --- /dev/null +++ b/test/screenplay_web/ensure_pa_message_admin_test.exs @@ -0,0 +1,23 @@ +defmodule ScreenplayWeb.EnsurePaMessageAdminTest do + use ScreenplayWeb.ConnCase + + describe "init/1" do + test "passes options through unchanged" do + assert ScreenplayWeb.EnsurePaMessageAdmin.init([]) == [] + end + end + + describe "call/2" do + @tag :authenticated_pa_message_admin + test "does nothing when user is a PA message admin", %{conn: conn} do + assert conn == ScreenplayWeb.EnsurePaMessageAdmin.call(conn, []) + end + + @tag :authenticated + test "redirects to Dashboard when user is not a PA message admin", %{conn: conn} do + conn = ScreenplayWeb.EnsurePaMessageAdmin.call(conn, []) + + assert redirected_to(conn) =~ "/dashboard" + end + end +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 524660d5..dc7ed203 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -61,6 +61,20 @@ defmodule ScreenplayWeb.ConnCase do {conn, user} + tags[:authenticated_pa_message_admin] -> + user = "test_user" + + conn = + Phoenix.ConnTest.build_conn() + |> Plug.Test.init_test_session(%{}) + |> Guardian.Plug.sign_in(ScreenplayWeb.AuthManager, user, %{ + "roles" => ["pa-message-admin"] + }) + |> Plug.Conn.put_session(:username, user) + |> assign(:roles, [:pa_message_admin]) + + {conn, user} + true -> {Phoenix.ConnTest.build_conn(), nil} end From 963610c30d21e55d51cc7220b17ce159e7c70325 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Tue, 2 Apr 2024 14:15:22 -0400 Subject: [PATCH 26/38] Use Plug instead of manual adding assigns. --- test/support/conn_case.ex | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index dc7ed203..bc161128 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -13,7 +13,6 @@ defmodule ScreenplayWeb.ConnCase do this option is not recommended for other databases. """ - import Plug.Conn use ExUnit.CaseTemplate using do @@ -45,7 +44,7 @@ defmodule ScreenplayWeb.ConnCase do "roles" => ["screenplay-emergency-admin"] }) |> Plug.Conn.put_session(:username, user) - |> assign(:roles, [:emergency_admin]) + |> Plug.run([{ScreenplayWeb.Plugs.Metadata, []}]) {conn, user} @@ -57,7 +56,7 @@ defmodule ScreenplayWeb.ConnCase do |> Plug.Test.init_test_session(%{}) |> Guardian.Plug.sign_in(ScreenplayWeb.AuthManager, user, %{roles: []}) |> Plug.Conn.put_session(:username, user) - |> assign(:roles, []) + |> Plug.run([{ScreenplayWeb.Plugs.Metadata, []}]) {conn, user} @@ -71,7 +70,7 @@ defmodule ScreenplayWeb.ConnCase do "roles" => ["pa-message-admin"] }) |> Plug.Conn.put_session(:username, user) - |> assign(:roles, [:pa_message_admin]) + |> Plug.run([{ScreenplayWeb.Plugs.Metadata, []}]) {conn, user} From ba3290ba7174c23965f7e34f926db7cfcc7af893 Mon Sep 17 00:00:00 2001 From: cmaddox5 Date: Fri, 19 Apr 2024 09:52:25 -0400 Subject: [PATCH 27/38] Added a doc for the endpoint RTS will use to retrieve active messages. --- docs/tech_specs/pa_message_api.md | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/tech_specs/pa_message_api.md diff --git a/docs/tech_specs/pa_message_api.md b/docs/tech_specs/pa_message_api.md new file mode 100644 index 00000000..e6d0e549 --- /dev/null +++ b/docs/tech_specs/pa_message_api.md @@ -0,0 +1,54 @@ +# List Active Messages + +Lists all PA messages that are currently eligible to play. + +**URL** : `/api/active_pa_messages` + +**Method** : `GET` + +**Parameters**: None + +**API key required** : YES + +## Success Responses + +**Code** : `200 OK` + +**Response**: + +```json +[ + { + "id": 1, + "sign_ids": ["sign_1", "sign2"], + "priority": 0, + "interval_in_minutes": 4, + "visual_text": "This message will be played.", + "audio_text": "This message will be played." + }, + { + "id": 2, + "sign_ids": ["sign_3", "sign4"], + "priority": 0, + "interval_in_minutes": 3, + "visual_text": "This message will be played.", + "audio_text": "This message will be played." + }, + { + "id": 3, + "sign_ids": ["sign_1"], + "priority": 0, + "interval_in_minutes": 2, + "visual_text": "This message will be played.", + "audio_text": "This message will be played." + } +] +``` + +## Failure Responses + +**Code** : `403 Forbidden` + +**Response**: + +`Invalid API key` From 5893bef37f5706b3e248cb03e1136efb8d7b390e Mon Sep 17 00:00:00 2001 From: Kim Date: Mon, 22 Apr 2024 14:54:30 -0400 Subject: [PATCH 28/38] add migration and dependencies --- config/config.exs | 8 ++++++++ lib/screenplay/repo.ex | 5 +++++ mix.exs | 4 +++- mix.lock | 5 +++++ 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 lib/screenplay/repo.ex diff --git a/config/config.exs b/config/config.exs index cb290efe..11a6cc9a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -7,6 +7,14 @@ # General application configuration import Config +config :screenplay, Screenplay.Repo, + database: "screenplay_repo", + username: "user", + password: "pass", + hostname: "localhost" + +config :screenplay, ecto_repos: [Screenplay.Repo] + # Configures the endpoint config :screenplay, ScreenplayWeb.Endpoint, url: [host: "localhost"], diff --git a/lib/screenplay/repo.ex b/lib/screenplay/repo.ex new file mode 100644 index 00000000..cc63e5bd --- /dev/null +++ b/lib/screenplay/repo.ex @@ -0,0 +1,5 @@ +defmodule Screenplay.Repo do + use Ecto.Repo, + otp_app: :screenplay, + adapter: Ecto.Adapters.Postgres +end diff --git a/mix.exs b/mix.exs index a9ab89cf..0c5932af 100644 --- a/mix.exs +++ b/mix.exs @@ -57,7 +57,9 @@ defmodule Screenplay.MixProject do {:lcov_ex, "~> 0.2", only: [:dev, :test], runtime: false}, {:sobelow, "~> 0.8", only: :dev}, {:sentry, "~> 8.0"}, - {:stream_data, "~> 0.5", only: :test} + {:stream_data, "~> 0.5", only: :test}, + {:ecto_sql, "~> 3.0"}, + {:postgrex, ">= 0.0.0"} ] end diff --git a/mix.lock b/mix.lock index 0da2f325..78f27495 100644 --- a/mix.lock +++ b/mix.lock @@ -6,7 +6,11 @@ "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.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [: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", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"}, + "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"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [: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", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, + "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [: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", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_aws": {:hex, :ex_aws, "2.5.1", "7418917974ea42e9e84b25e88b9f3d21a861d5f953ad453e212f48e593d8d39f", [: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", "1b95431f70c446fa1871f0eb9b183043c5a625f75f9948a42d25f43ae2eff12b"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, @@ -36,6 +40,7 @@ "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.6.2", "753611b23b29231fb916b0cdd96028084b12aff57bfd7b71781bd04b1dbeb5c9", [: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", "951ed2433df22f4c97b85fdb145d4cee561f36b74854d64c06d896d7cd2921a7"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, + "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "sentry": {:hex, :sentry, "8.1.0", "8d235b62fce5f8e067ea1644e30939405b71a5e1599d9529ff82899d11d03f2b", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "f9fc7641ef61e885510f5e5963c2948b9de1de597c63f781e9d3d6c9c8681ab4"}, "sftp_client": {:hex, :sftp_client, "1.4.7", "7b210a154a1929d145a73c8e490f5915f1ed2031fbc4ae12ed3e623122fa6825", [:mix], [], "hexpm", "1d54e28476e7ed71529f8e463aa5b8dfcef6390d798ffad5510299a368cc8345"}, From 5ffb3e28cb16fb8d8a1facf77047969f55b04107 Mon Sep 17 00:00:00 2001 From: Kim Date: Tue, 23 Apr 2024 17:03:00 -0400 Subject: [PATCH 29/38] Add migration and database configuration --- .gitignore | 1 + config/config.exs | 11 ++++-- config/prod.exs | 6 +++ config/runtime.exs | 2 + config/test.exs | 5 +++ lib/screenplay/repo.ex | 38 +++++++++++++++++++ mix.exs | 1 + mix.lock | 1 + ...20240422170503_create_pa_message_table.exs | 25 ++++++++++++ 9 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 priv/repo/migrations/20240422170503_create_pa_message_table.exs diff --git a/.gitignore b/.gitignore index b1bbda65..23826d65 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,6 @@ npm-debug.log # All files in /priv/ are going to be for local development # so do not need to be tracked. /priv/* +!priv/repo* .envrc diff --git a/config/config.exs b/config/config.exs index 11a6cc9a..21b90e26 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,10 +8,13 @@ import Config config :screenplay, Screenplay.Repo, - database: "screenplay_repo", - username: "user", - password: "pass", - hostname: "localhost" + database: "screenplay_dev", + username: System.get_env("DATABASE_USER", ""), + password: System.get_env("DATABASE_PASSWORD", ""), + hostname: System.get_env("DATABASE_HOST", "localhost"), + port: System.get_env("DATABASE_PORT", "5432") |> String.to_integer(), + show_sensitive_data_on_connection_error: true, + backoff_min: 5_000 config :screenplay, ecto_repos: [Screenplay.Repo] diff --git a/config/prod.exs b/config/prod.exs index cb88e533..f1dbd483 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -31,6 +31,12 @@ config :ueberauth, Ueberauth, {Ueberauth.Strategy.Oidcc, userinfo: true, uid_field: "email", scopes: ~w(openid email)} ] +config :screenplay, Screenplay.Repo, + database: "screenplay", + ssl: true, + show_sensitive_data_on_connection_error: false, + configure: {Screenplay.Repo, :add_prod_credentials, []} + # ## SSL Support # # To get SSL working, you will need to add the `https` key diff --git a/config/runtime.exs b/config/runtime.exs index 31b624e7..778cb20b 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -56,3 +56,5 @@ config :sentry, included_environments: [env], enable_source_code_context: true, root_source_code_path: File.cwd!() + +config :screenplay, Screenplay.Repo, pool_size: 10 diff --git a/config/test.exs b/config/test.exs index c6349b59..64d35881 100644 --- a/config/test.exs +++ b/config/test.exs @@ -27,5 +27,10 @@ config :ueberauth_oidcc, ] ] +config :screenplauy, Screenplay.Repo, + adapter: Ecto.Adapters.Postgres, + database: "screenplay_test", + pool: Ecto.Adapters.SQL.Sandbox + # Print only warnings and errors during test config :logger, level: :warning diff --git a/lib/screenplay/repo.ex b/lib/screenplay/repo.ex index cc63e5bd..3cbb168b 100644 --- a/lib/screenplay/repo.ex +++ b/lib/screenplay/repo.ex @@ -1,5 +1,43 @@ defmodule Screenplay.Repo do + require Logger + use Ecto.Repo, otp_app: :screenplay, adapter: Ecto.Adapters.Postgres + + def add_prod_credentials(config, auth_token_fn \\ &ExAws.RDS.generate_db_auth_token/4) do + host = System.get_env("DATABASE_HOST") + port = String.to_integer(System.get_env("DATABASE_PORT", "5432")) + user = System.get_env("DATABASE_USER") + + token = + auth_token_fn.( + host, + user, + port, + %{} + ) + + if is_nil(token) do + Logger.info("#{__MODULE__} add_prod_credentials token_is_nil") + else + hash_string = Base.encode16(:crypto.hash(:sha3_256, token)) + + Logger.info("#{__MODULE__} add_prod_credentials token_hash=#{hash_string}") + end + + Keyword.merge(config, + hostname: host, + username: user, + port: port, + password: token, + ssl_opts: [ + cacertfile: "priv/aws-cert-bundle.pem", + verify: :verify_peer, + server_name_indication: String.to_charlist(host), + verify_fun: + {&:ssl_verify_hostname.verify_fun/3, [check_hostname: String.to_charlist(host)]} + ] + ) + end end diff --git a/mix.exs b/mix.exs index 0c5932af..2a22046e 100644 --- a/mix.exs +++ b/mix.exs @@ -53,6 +53,7 @@ defmodule Screenplay.MixProject do {:sftp_client, "~> 1.4"}, {:ex_aws, "~> 2.5"}, {:ex_aws_s3, "~> 2.5"}, + {:ex_aws_rds, "~> 2.0.2"}, {:httpoison, "~> 1.8.0"}, {:lcov_ex, "~> 0.2", only: [:dev, :test], runtime: false}, {:sobelow, "~> 0.8", only: :dev}, diff --git a/mix.lock b/mix.lock index 78f27495..883a47a2 100644 --- a/mix.lock +++ b/mix.lock @@ -13,6 +13,7 @@ "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [: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", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_aws": {:hex, :ex_aws, "2.5.1", "7418917974ea42e9e84b25e88b9f3d21a861d5f953ad453e212f48e593d8d39f", [: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", "1b95431f70c446fa1871f0eb9b183043c5a625f75f9948a42d25f43ae2eff12b"}, + "ex_aws_rds": {:hex, :ex_aws_rds, "2.0.2", "38dd8e83d57cf4b7286c4f6f5c978f700c40c207ffcdd6ca5d738e5eba933f9a", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm", "9e5b5cc168077874cbd0d29ba65d01caf1877e705fb5cecacf0667dd19bfa75c"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, "expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, diff --git a/priv/repo/migrations/20240422170503_create_pa_message_table.exs b/priv/repo/migrations/20240422170503_create_pa_message_table.exs new file mode 100644 index 00000000..8cc55940 --- /dev/null +++ b/priv/repo/migrations/20240422170503_create_pa_message_table.exs @@ -0,0 +1,25 @@ +defmodule Screenplay.Repo.Migrations.CreatePaMessageTable do + use Ecto.Migration + + def change do + create table("pa_message") do + add :alert_id, :string + add :start_time, :utc_datetime + add :end_time, :utc_datetime + add :days_of_week, {:array, :string} + add :sign_ids, {:array, :string}, null: false + add :priority, :integer, null: false + add :interval_in_minutes, :integer, null: false + add :visual_text, :text, null: false + add :audio_text, :text, null: false + add :paused, :boolean + add :saved, :boolean + add :message_type, :string + + timestamps(type: :utc_datetime) + end + + create index("pa_message", [:start_time, :end_time]) + create index("pa_message", ["(to_tsvector('english', visual_text))"], using: "GIN") + end +end From 2a830b1c54952efe52d08d2c63c9524913fdc696 Mon Sep 17 00:00:00 2001 From: Kim Date: Tue, 23 Apr 2024 17:51:25 -0400 Subject: [PATCH 30/38] Add documentation for postgres --- .envrc.template | 6 ++++++ README.md | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/.envrc.template b/.envrc.template index 6d5cacbb..69313693 100644 --- a/.envrc.template +++ b/.envrc.template @@ -15,3 +15,9 @@ # export SCREENS_URL= # export SIGNS_UI_URL= # export ALERTS_UI_URL= + +## Postgres configuration: username, password, and hostname +## * Your local Postgres server should go here +# export DATABASE_USERNAME= +# export DATABASE_PASSWORD= +# export DATABASE_HOSTNAME= diff --git a/README.md b/README.md index 07644ebc..9b0be0a6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ This tool enables PIOs to upload urgent messages to the Outfront signs in and outside stations. +## Prerequisites + +Screenplay requires Postgres. If you don't already have Postgres installed, and you're on a Mac, [Postgres.app](https://postgresapp.com/downloads.html) is an easy way to get started. However, any Postgres instance to which you can connect and in which you have sufficient privileges should work. + ## Development To start your Phoenix server: From f8163e7196058f2ee3734e65e9b6de0e2af8d523 Mon Sep 17 00:00:00 2001 From: Kim Date: Wed, 24 Apr 2024 16:29:15 -0400 Subject: [PATCH 31/38] Add migrations process --- lib/screenplay/application.ex | 4 ++- lib/screenplay/migrate.ex | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 lib/screenplay/migrate.ex diff --git a/lib/screenplay/application.ex b/lib/screenplay/application.ex index 1623d6c8..a5468f19 100644 --- a/lib/screenplay/application.ex +++ b/lib/screenplay/application.ex @@ -17,7 +17,9 @@ defmodule Screenplay.Application do # {Screenplay.Worker, arg} Screenplay.OutfrontTakeoverTool.Alerts.State, Screenplay.OutfrontTakeoverTool.Alerts.Reminders, - Screenplay.Scheduler + Screenplay.Scheduler, + Screenplay.Repo, + Screenplay.Migrate ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/screenplay/migrate.ex b/lib/screenplay/migrate.ex new file mode 100644 index 00000000..02503fed --- /dev/null +++ b/lib/screenplay/migrate.ex @@ -0,0 +1,53 @@ +defmodule Screenplay.Migrate do + @moduledoc """ + GenServer which runs on startup to run Ecto migrations. All migrations + stored in the "migrations" directory are run during init. Migrations stored + in the "async_migrations" directory will be run after the regular migrations + complete and will only log a warning on failure. + """ + use GenServer, restart: :transient + require Logger + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts) + end + + @impl GenServer + def init(opts) do + Logger.info("#{__MODULE__} synchronous migrations starting") + Keyword.get(opts, :sync_migrate_fn, &default_migrate_fn/1).("migrations") + + Logger.info("#{__MODULE__} synchronous migrations finished") + {:ok, opts, {:continue, :async_migrations}} + end + + @impl GenServer + def handle_continue(:async_migrations, opts) do + Logger.info("#{__MODULE__} async migrations starting") + + try do + Keyword.get( + opts, + :async_migrate_fn, + &default_migrate_fn/1 + ).("async_migrations") + + Logger.info("#{__MODULE__} async migrations finished") + rescue + e -> + Logger.warning("#{__MODULE__} async migrations failed. error=#{inspect(e)}") + :ok + end + + {:stop, :normal, opts} + end + + defp default_migrate_fn(migration_directory) do + Ecto.Migrator.run( + Screenplay.Repo, + Ecto.Migrator.migrations_path(Screenplay.Repo, migration_directory), + :up, + all: true + ) + end +end From e2f93fe9edd74f5bd8d9576327ce3edab53269c9 Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Thu, 25 Apr 2024 10:06:25 -0400 Subject: [PATCH 32/38] Update config/test.exs Co-authored-by: Christian Maddox --- config/test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/test.exs b/config/test.exs index 1cbea0ce..7360e4d3 100644 --- a/config/test.exs +++ b/config/test.exs @@ -28,7 +28,7 @@ config :ueberauth_oidcc, ] ] -config :screenplauy, Screenplay.Repo, +config :screenplay, Screenplay.Repo, adapter: Ecto.Adapters.Postgres, database: "screenplay_test", pool: Ecto.Adapters.SQL.Sandbox From f997b2e3cdea2a9df6d8b59d13655a8e2beab2da Mon Sep 17 00:00:00 2001 From: Kim Date: Thu, 25 Apr 2024 12:38:35 -0400 Subject: [PATCH 33/38] remove async migrations method --- .envrc.template | 2 +- lib/screenplay/migrate.ex | 23 +---------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/.envrc.template b/.envrc.template index 69313693..c3d80445 100644 --- a/.envrc.template +++ b/.envrc.template @@ -18,6 +18,6 @@ ## Postgres configuration: username, password, and hostname ## * Your local Postgres server should go here -# export DATABASE_USERNAME= +# export DATABASE_USER= # export DATABASE_PASSWORD= # export DATABASE_HOSTNAME= diff --git a/lib/screenplay/migrate.ex b/lib/screenplay/migrate.ex index 02503fed..8863cf6e 100644 --- a/lib/screenplay/migrate.ex +++ b/lib/screenplay/migrate.ex @@ -18,28 +18,7 @@ defmodule Screenplay.Migrate do Keyword.get(opts, :sync_migrate_fn, &default_migrate_fn/1).("migrations") Logger.info("#{__MODULE__} synchronous migrations finished") - {:ok, opts, {:continue, :async_migrations}} - end - - @impl GenServer - def handle_continue(:async_migrations, opts) do - Logger.info("#{__MODULE__} async migrations starting") - - try do - Keyword.get( - opts, - :async_migrate_fn, - &default_migrate_fn/1 - ).("async_migrations") - - Logger.info("#{__MODULE__} async migrations finished") - rescue - e -> - Logger.warning("#{__MODULE__} async migrations failed. error=#{inspect(e)}") - :ok - end - - {:stop, :normal, opts} + {:ok, opts} end defp default_migrate_fn(migration_directory) do From 4086514bd73d7e14f3b07c255af4a09690142a2b Mon Sep 17 00:00:00 2001 From: Kim Date: Thu, 25 Apr 2024 14:31:36 -0400 Subject: [PATCH 34/38] Add postgres service to CI --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a592f4b7..f17b3ade 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,8 +28,22 @@ jobs: name: Build and test runs-on: ubuntu-latest env: + DATABASE_PASSWORD: postgres + DATABASE_USER: postgres + DATABASE_NAME: screenplay_test + DATABASE_HOST: localhost GUARDIAN_SECRET_KEY: test_auth_secret SECRET_KEY_BASE: local_secret_key_base_at_least_64_bytes_________________________________ + services: + postgres: + image: postgres + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: ${{env.DATABASE_PASSWORD}} + POSTGRES_USER: ${{env.DATABASE_USER}} + POSTGRES_DB: ${{env.DATABASE_NAME}} + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 needs: asdf steps: - uses: actions/checkout@v2 From e8f60c61b4e64c4abd6167882961426375cfd445 Mon Sep 17 00:00:00 2001 From: Kim Date: Thu, 25 Apr 2024 15:40:23 -0400 Subject: [PATCH 35/38] formatting --- assets/js/components/Dashboard/ScreenDetailActionBar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/js/components/Dashboard/ScreenDetailActionBar.tsx b/assets/js/components/Dashboard/ScreenDetailActionBar.tsx index c598cc2c..c58f832c 100644 --- a/assets/js/components/Dashboard/ScreenDetailActionBar.tsx +++ b/assets/js/components/Dashboard/ScreenDetailActionBar.tsx @@ -43,11 +43,11 @@ const CustomToggle = React.forwardRef( {props.children} - ) + ), ); const ScreenDetailActionBar = ( - props: ScreenDetailActionBarProps + props: ScreenDetailActionBarProps, ): JSX.Element => { const dropdownRef = useRef(null); const [isOpen, setIsOpen] = useState(false); @@ -77,12 +77,12 @@ const ScreenDetailActionBar = ( type: "SHOW_LINK_COPIED", showLinkCopied: false, }), - 5000 + 5000, ); }; const isEmergencyAdmin = document.querySelector( - "meta[name=is-emergency-admin]" + "meta[name=is-emergency-admin]", ); const reportAProblemURL = isEmergencyAdmin From ec0b61b81184fbabf604cd932af71695fbb8ebb6 Mon Sep 17 00:00:00 2001 From: Kim Date: Thu, 25 Apr 2024 16:55:36 -0400 Subject: [PATCH 36/38] fetch cert --- Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dockerfile b/Dockerfile index 026ff709..60c20f5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,8 +30,14 @@ FROM elixir-builder as app-builder ENV LANG="C.UTF-8" MIX_ENV="prod" +RUN apk add --no-cache --update curl + WORKDIR /root +RUN curl https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \ + -o aws-cert-bundle.pem +RUN echo "51b107da46717aed974d97464b63f7357b220fe8737969db1492d1cae74b3947 aws-cert-bundle.pem" | sha256sum -c - + # add frontend assets compiled in node container, required by phx.digest COPY --from=assets-builder /root/priv/static ./priv/static From 55a4c9afd8789f6fc2e6768742c1bc9e6dc4a6ba Mon Sep 17 00:00:00 2001 From: Kim Date: Thu, 25 Apr 2024 17:07:58 -0400 Subject: [PATCH 37/38] move cert file to priv --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 60c20f5b..36fdf72b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,5 +63,7 @@ COPY --from=app-builder /root/priv/static ./priv/static # add application artifact comipled in app build container COPY --from=app-builder /root/_build/prod/rel/screenplay . +COPY --from=app-builder --chown=skate:skate /root/aws-cert-bundle.pem ./priv/aws-cert-bundle.pem + # run the application CMD ["bin/screenplay", "start"] From 015760f65fe911f06cd67d172b7a9604285b8c90 Mon Sep 17 00:00:00 2001 From: Kim Date: Thu, 25 Apr 2024 17:15:46 -0400 Subject: [PATCH 38/38] fix typo --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 36fdf72b..6c5f1fcf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,7 +63,7 @@ COPY --from=app-builder /root/priv/static ./priv/static # add application artifact comipled in app build container COPY --from=app-builder /root/_build/prod/rel/screenplay . -COPY --from=app-builder --chown=skate:skate /root/aws-cert-bundle.pem ./priv/aws-cert-bundle.pem +COPY --from=app-builder --chown=screenplay:screenplay /root/aws-cert-bundle.pem ./priv/aws-cert-bundle.pem # run the application CMD ["bin/screenplay", "start"]