Packages
outerfaces_odd
0.2.4
Outerfaces ODD (Outerfaces Dependency Distribution) -- Extensions for Outerfaces framework
Current section
Files
Jump to
Current section
Files
lib/plugs/odd_environment_plug.ex
defmodule Outerfaces.Odd.Plugs.OddEnvironmentPlug do
@moduledoc false
import Plug.Conn
alias Plug.Conn
require Logger
@spec init(Keyword.t()) :: Keyword.t()
def init(opts \\ []), do: opts
@spec call(Conn.t(), Keyword.t()) :: Conn.t()
def call(%Conn{request_path: "/environments/outerfaces_environment.js"} = conn, opts) do
origin = get_req_header(conn, "origin") |> List.first()
# Fallback to Host header if no Origin (for same-origin requests)
origin =
if is_nil(origin) do
host = get_req_header(conn, "host") |> List.first()
if host, do: "https://#{host}", else: nil
else
origin
end
conn
|> put_resp_content_type("application/javascript")
|> send_resp(200, build_pseudo_file(opts, origin))
|> halt()
end
def call(conn, _opts), do: conn
@spec build_pseudo_file(Keyword.t(), String.t() | nil) :: String.t()
defp build_pseudo_file(opts, origin) do
# Check for unified proxy mode - returns origin-based URLs
unified_proxy_mode = Keyword.get(opts, :unified_proxy_mode, false)
if unified_proxy_mode do
build_unified_proxy_pseudo_file(opts)
else
build_multi_port_pseudo_file(opts, origin)
end
end
# Unified proxy mode - all services under single origin
# The frontend adds /api prefix itself, so we just provide the origin
defp build_unified_proxy_pseudo_file(opts) do
extra_key_value_pairs = Keyword.get(opts, :extra_key_value_pairs, [])
extra_keypairs_formatted =
extra_key_value_pairs
|> format_extra_keypairs()
if extra_keypairs_formatted == "" do
"""
export default {
outerfaces_cdn_url: window.location.origin + '/cdn',
outerfaces_api_url: window.location.origin
}
"""
else
"""
export default {
outerfaces_cdn_url: window.location.origin + '/cdn',
outerfaces_api_url: window.location.origin,
api_host: window.location.host,
#{extra_keypairs_formatted}
}
"""
end
end
# Multi-port mode - separate ports for each service
defp build_multi_port_pseudo_file(opts, origin) do
protocol = Keyword.get(opts, :protocol)
host_names = Keyword.get(opts, :host_names)
cdn_port = Keyword.get(opts, :cdn_port)
api_port = Keyword.get(opts, :api_port)
extra_key_value_pairs = Keyword.get(opts, :extra_key_value_pairs, [])
origin = (origin && strip_protocol_and_port(origin)) || nil
origin_lower = origin && String.downcase(origin)
# Case-insensitive matching
host_name =
if origin_lower do
Enum.find(host_names, fn host ->
String.downcase(host) == origin_lower
end)
else
nil
end
# Log and default to first hostname if no match
host_name =
if host_name do
Logger.debug("[OddEnvironmentPlug] Origin '#{origin}' matched hostname '#{host_name}'")
host_name
else
default = Enum.at(host_names, 0)
if origin do
Logger.warning(
"[OddEnvironmentPlug] Origin '#{origin}' did not match any hostname in #{inspect(host_names)}, defaulting to '#{default}'"
)
else
Logger.debug(
"[OddEnvironmentPlug] No origin header, using default hostname '#{default}'"
)
end
default
end
build_pseudo_file(protocol, host_name, cdn_port, api_port, extra_key_value_pairs)
end
@spec build_pseudo_file(String.t(), String.t(), integer(), integer(), [{String.t(), any()}]) ::
String.t()
defp build_pseudo_file(protocol, host_name, cdn_port, api_port, []) do
"""
export default {
outerfaces_cdn_url: '#{build_origin_path(protocol, host_name, cdn_port)}',
outerfaces_api_url: '#{build_origin_path(protocol, host_name, api_port)}'
}
"""
end
defp build_pseudo_file(protocol, host_name, cdn_port, api_port, extra_keypairs)
when is_binary(protocol) and is_binary(host_name) and
is_integer(cdn_port) and is_integer(api_port) and
is_list(extra_keypairs) do
extra_keypairs_formatted = format_extra_keypairs(extra_keypairs)
"""
export default {
outerfaces_cdn_url: '#{build_origin_path(protocol, host_name, cdn_port)}',
outerfaces_api_url: '#{build_origin_path(protocol, host_name, api_port)}',
api_host: '#{host_name}:#{api_port}',
#{extra_keypairs_formatted}
}
"""
end
defp format_extra_keypairs([]), do: ""
defp format_extra_keypairs(extra_keypairs) do
extra_keypairs
|> Enum.map(fn
# Support dynamic values via MFA tuples {Module, :function, args}
{key, {module, function, args}} when is_atom(module) and is_atom(function) and is_list(args) ->
resolved_value = apply(module, function, args)
"#{do_normalize_key(key)}: #{do_normalize_value(resolved_value)}"
{key, value} when is_list(value) ->
formatted_values =
value
|> Enum.map(&do_normalize_value/1)
|> Enum.join(", ")
"#{do_normalize_key(key)}: [#{formatted_values}]"
{key, value} ->
"#{do_normalize_key(key)}: #{do_normalize_value(value)}"
end)
|> Enum.join(",\n ")
|> String.trim()
end
@spec do_normalize_key(String.t() | atom()) :: String.t() | nil
defp do_normalize_key(key) do
case key do
k when is_binary(k) ->
"#{k}" |> String.replace(" ", "_")
k when is_atom(k) ->
k |> Atom.to_string() |> String.replace(" ", "_")
_ ->
nil
end
end
@spec do_normalize_value(any()) :: String.t() | nil
defp do_normalize_value(value) do
case value do
v when is_binary(v) -> "'#{v}'"
v when is_integer(v) -> "#{v}"
v when is_boolean(v) -> "#{v}"
v when is_float(v) -> "#{v}"
_ -> nil
end
end
@spec build_origin_path(String.t(), String.t(), integer()) :: String.t()
defp build_origin_path(protocol, host, port) do
"#{protocol}://#{host}:#{port}"
end
@spec strip_protocol_and_port(String.t()) :: String.t()
defp strip_protocol_and_port(origin) do
origin
|> String.split("://")
|> List.last()
|> String.split(":")
|> List.first()
end
end