Packages
A Fair Source multi-agent runtime with deterministic agent scoring and replayable run history.
Retired package: Deprecated - superseded — operator console moved to the Syntropy app
Current section
Files
Jump to
Current section
Files
lib/syntropy_web/operator_auth.ex
defmodule SyntropyWeb.OperatorAuth do
@moduledoc """
Session-based operator authentication for the mission control UI.
Credentials come from the environment (`SYNTROPY_OPERATOR_PASSWORD`, plus
an optional `SYNTROPY_OPERATOR_USER`, default `"operator"`). Enforcement is
required outside dev/test, mirroring `SyntropyWeb.ApiAuth.required?/0`.
"""
import Phoenix.Component, only: [assign: 3]
@session_key "operator_authenticated"
@spec required?() :: boolean()
def required? do
config()
|> Keyword.get(:required, false)
end
@spec username() :: String.t()
def username do
config()
|> Keyword.get(:username, "operator")
end
@doc "Validates a login attempt in constant time."
@spec validate_credentials(String.t() | nil, String.t() | nil) :: :ok | {:error, :invalid}
def validate_credentials(provided_username, provided_password)
when is_binary(provided_username) and is_binary(provided_password) do
expected_password = Keyword.get(config(), :password)
with true <- is_binary(expected_password) and expected_password != "",
true <- secure_equal?(provided_username, username()),
true <- secure_equal?(provided_password, expected_password) do
:ok
else
_mismatch -> {:error, :invalid}
end
end
def validate_credentials(_username, _password), do: {:error, :invalid}
@spec session_key() :: String.t()
def session_key, do: @session_key
@spec sign_in(Plug.Conn.t()) :: Plug.Conn.t()
def sign_in(conn) do
conn
|> Plug.Conn.configure_session(renew: true)
|> Plug.Conn.put_session(@session_key, true)
end
@spec sign_out(Plug.Conn.t()) :: Plug.Conn.t()
def sign_out(conn) do
Plug.Conn.configure_session(conn, drop: true)
end
@spec authenticated?(Plug.Conn.t() | map()) :: boolean()
def authenticated?(%Plug.Conn{} = conn) do
Plug.Conn.get_session(conn, @session_key) == true
end
def authenticated?(session) when is_map(session) do
Map.get(session, @session_key) == true
end
@doc """
LiveView mount guard: `live_session ..., on_mount: {SyntropyWeb.OperatorAuth, :default}`.
"""
@spec on_mount(:default, map(), map(), Phoenix.LiveView.Socket.t()) ::
{:cont, Phoenix.LiveView.Socket.t()} | {:halt, Phoenix.LiveView.Socket.t()}
def on_mount(:default, _params, session, socket) do
if not required?() or authenticated?(session) do
{:cont, assign(socket, :operator_auth_required, required?())}
else
{:halt, Phoenix.LiveView.redirect(socket, to: "/login")}
end
end
defp secure_equal?(left, right) when is_binary(left) and is_binary(right) do
byte_size(left) == byte_size(right) and Plug.Crypto.secure_compare(left, right)
end
defp secure_equal?(_left, _right), do: false
defp config do
Application.get_env(:syntropy, __MODULE__, [])
end
end