Current section

Files

Jump to
electric lib electric plug router.ex
Raw

lib/electric/plug/router.ex

defmodule Electric.Plug.Router do
use Plug.Router, copy_opts_to_assign: :config
use Electric.Telemetry
with_telemetry Sentry.PlugCapture do
use Sentry.PlugCapture
end
alias Electric.Plug.Utils.CORSHeaderPlug
alias Electric.Plug.Utils.PassAssignToOptsPlug
plug Plug.RequestId, assign_as: :plug_request_id
plug :server_header, Electric.version()
plug :add_stack_id_to_metadata
# converts HEAD requests to GET requests
plug Plug.Head
plug RemoteIp
plug :match
plug Electric.Plug.LabelProcessPlug
plug Electric.Plug.TraceContextPlug
plug Plug.Telemetry, event_prefix: [:electric, :routing]
plug Plug.Logger
with_telemetry Sentry.PlugCapture do
plug Sentry.PlugContext
end
plug :authenticate
plug :put_cors_headers
plug :dispatch
match "/", via: [:get, :head], do: send_resp(conn, 200, "")
get "/v1/shape",
to: PassAssignToOptsPlug,
init_opts: [plug: Electric.Plug.ServeShapePlug, assign_key: :config]
delete "/v1/shape",
to: PassAssignToOptsPlug,
init_opts: [plug: Electric.Plug.DeleteShapePlug, assign_key: :config]
options "/v1/shape", to: Electric.Plug.OptionsShapePlug
get "/v1/health", to: Electric.Plug.HealthCheckPlug
match _, do: send_resp(conn, 404, "Not found")
def server_header(conn, version),
do: conn |> Plug.Conn.put_resp_header("electric-server", "ElectricSQL/#{version}")
# OPTIONS requests should not be authenticated
def authenticate(%Plug.Conn{method: "OPTIONS"} = conn, _opts), do: conn
def authenticate(%Plug.Conn{request_path: "/v1/shape"} = conn, _opts) do
api_secret = conn.assigns.config[:secret]
if is_nil(api_secret) do
# We're in insecure mode, so we don't need to authenticate
conn
else
conn = conn |> fetch_query_params()
# Keep `api_secret` for backwards compatibility
# We'll remove it when we release v2
case conn.query_params["secret"] || conn.query_params["api_secret"] do
^api_secret ->
conn
_ ->
conn
|> send_resp(401, Jason.encode!(%{message: "Unauthorized - Invalid API secret"}))
|> halt()
end
end
end
# For unmatched routes, just pass through
def authenticate(conn, _opts), do: conn
def put_cors_headers(%Plug.Conn{path_info: ["v1", "shape" | _]} = conn, _opts),
do: CORSHeaderPlug.call(conn, %{methods: ["GET", "HEAD", "DELETE", "OPTIONS"]})
def put_cors_headers(conn, _opts),
do: CORSHeaderPlug.call(conn, %{methods: ["GET", "HEAD"]})
def add_stack_id_to_metadata(conn, _) do
Logger.metadata(stack_id: conn.assigns.config[:stack_id])
Electric.Telemetry.Sentry.set_tags_context(stack_id: conn.assigns.config[:stack_id])
conn
end
end