defmodule BreakGlass.Supervisor do @moduledoc """ OTP supervisor for the `break_glass_ex` library. Add this supervisor to your host application's supervision tree **before** your endpoint or router to ensure all ETS tables exist before the first authentication request: children = [ # ... database repos, etc. ... {BreakGlass.Supervisor, []}, MyAppWeb.Endpoint ] The supervisor starts `RateLimiter`, `TokenStore`, and `OtpStore` in that order using a `:one_for_one` strategy. It validates required configuration keys at startup and raises `ArgumentError` if any are missing, preventing the application from booting in a misconfigured state. ## Required Configuration The following keys must be present under `config :break_glass_ex`: - `:email` — the break-glass email address (`String.t()`) - `:password_hash` — the bcrypt hash of the break-glass password (`String.t()`) - `:user_provider` — the module implementing `BreakGlass.UserProvider` (`module()`) If any required key is absent, an `ArgumentError` is raised listing **all** missing keys before any child GenServer is started: ArgumentError: BreakGlass: missing required configuration keys: [:email, :password_hash]. Add them under `config :break_glass_ex` in your runtime.exs. ## Optional Configuration - `:allowed_ips` — list of exact IPs or CIDR ranges allowed to authenticate (default: `["127.0.0.1", "::1"]`). If absent, a `Logger.warning` is emitted at startup. """ use Supervisor require Logger @required_keys [:email, :password_hash, :user_provider] # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- @doc """ Starts the `BreakGlass.Supervisor` and links it to the calling process. `opts` are passed through to `Supervisor.start_link/3` but are otherwise unused by the supervisor itself. """ @spec start_link(keyword()) :: Supervisor.on_start() def start_link(opts \\ []) do Supervisor.start_link(__MODULE__, opts, name: __MODULE__) end # --------------------------------------------------------------------------- # Supervisor callbacks # --------------------------------------------------------------------------- @impl true def init(_opts) do config = Application.get_all_env(:break_glass_ex) validate_config!(config) warn_if_no_allowed_ips(config) children = [ BreakGlass.RateLimiter, BreakGlass.TokenStore, BreakGlass.OtpStore ] Supervisor.init(children, strategy: :one_for_one) end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp validate_config!(config) do config_keys = Keyword.keys(config) missing = Enum.reject(@required_keys, &(&1 in config_keys)) unless missing == [] do raise ArgumentError, "BreakGlass: missing required configuration keys: #{inspect(missing)}. " <> "Add them under `config :break_glass_ex` in your runtime.exs." end end defp warn_if_no_allowed_ips(config) do unless Keyword.has_key?(config, :allowed_ips) do Logger.warning( "[BreakGlass] :allowed_ips is not configured — defaulting to localhost " <> "(127.0.0.1 and ::1). Set `config :break_glass_ex, allowed_ips: [...]` " <> "in your runtime.exs to restrict access to specific IPs or CIDR ranges." ) end end end