Skip to main content

lib/ez_auth.ex

defmodule EzAuth do
  @moduledoc """
  Top-level Phoenix and LiveView integration for EzAuth.
  """

  import EzAuth.Translations, only: [translate: 1]

  alias EzAuth.Config
  alias EzAuth.Scopes.UserScope

  def on_mount(:assign_current_scope, _params, session, socket) do
    {:cont, assign_current_scope(socket, session)}
  end

  def on_mount(:require_authenticated, _params, session, socket) do
    socket = assign_current_scope(socket, session)

    if get_in(socket.assigns.current_scope.user) do
      {:cont, socket}
    else
      socket =
        socket
        |> Phoenix.LiveView.put_flash(:error, translate("You must log in to access this page."))
        |> Phoenix.LiveView.redirect(to: Config.sign_in_path())

      {:halt, socket}
    end
  end

  defmacro __using__(_opts) do
    quote do
      import EzAuth.Auth,
        only: [
          fetch_current_scope: 2,
          require_authenticated: 2,
          redirect_if_authenticated: 1
        ]

      import EzAuth, only: [auth_routes: 0, auth_routes: 1]
    end
  end

  defmacro auth_routes(opts \\ []) do
    handler = Keyword.get(opts, :handler)
    strategies = EzAuth.Strategy.supported_strategies()

    quote do
      EzAuth.__validate_scope__!(__MODULE__)

      scope "/auth", alias: false, as: false do
        for strategy <- unquote(strategies) do
          slug = EzAuth.Strategy.slug(strategy)

          post("/#{slug}/request", EzAuth.Dispatcher, :request,
            private: %{ez_auth: %{strategy: strategy, handler: unquote(handler)}}
          )

          for method <- strategy.__meta__(:callback_methods) do
            match(method, "/#{slug}/callback", EzAuth.Dispatcher, :callback,
              private: %{ez_auth: %{strategy: strategy, handler: unquote(handler)}}
            )
          end
        end

        post("/sign-up", EzAuth.Dispatcher, :sign_up,
          private: %{ez_auth: %{handler: unquote(handler)}}
        )

        delete("/sign-out", EzAuth.Dispatcher, :sign_out,
          private: %{ez_auth: %{handler: unquote(handler)}}
        )
      end
    end
  end

  def __validate_scope__!(module) do
    if Enum.any?(scopes(module), &problematic?/1) do
      raise CompileError,
        description: """
        auth_routes/1 must be invoked at the router's top level, or inside a top level scope.
        Remove the wrapping scope so the auth routes can be correctly registered.
        """
    end
  end

  defp scopes(module) do
    List.wrap(
      Module.get_attribute(module, :phoenix_top_scopes) ||
        Module.get_attribute(module, :phoenix_router_scopes)
    )
  end

  defp problematic?(%{path: p, alias: a, as: as}) do
    p not in [nil, "", "/", []] or a not in [nil, []] or as not in [nil, []]
  end

  defp assign_current_scope(socket, session) do
    Phoenix.Component.assign_new(socket, :current_scope, fn ->
      case EzAuth.Accounts.get_user_by_session_token(session["user_token"]) do
        :error -> nil
        user -> UserScope.new(user)
      end
    end)
  end
end