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