lib/mix/tasks/boruta.gen.controllers.ex

defmodule Mix.Tasks.Boruta.Gen.Controllers do
  @moduledoc """
  This task will help creation of a basic OAuth/OpenID Connect server by providing needed phoenix controllers, views and templates to expose OAuth endpoints.

  Controllers are unit tested using Mox, you'll need to add that dependency in order to run them (see below).

  ## Examples
  ```
  mix boruta.gen.controllers
  ```

  ## Post installation steps

  * You can add OAuth routes in web application router as follow to expose controller actions

  ```elixir
  scope "/oauth", MyAppWeb.Oauth do
    pipe_through :api

    post "/revoke", RevokeController, :revoke
    post "/token", TokenController, :token
    post "/introspect", IntrospectController, :introspect
  end

  scope "/openid", MyAppWeb.Openid do
    pipe_through [:api]

    get "/userinfo", UserinfoController, :userinfo
    post "/userinfo", UserinfoController, :userinfo
    get "/jwks", JwksController, :jwks_index
  end

  ####

  scope "/oauth", MyAppWeb.Oauth do
    pipe_through [:browser, :fetch_current_user]

    get "/authorize", AuthorizeController, :authorize
  end

  ## OR

  scope "/openid", MyAppWeb.Openid do
    pipe_through [:browser, :fetch_current_user]

    get "/authorize", AuthorizeController, :authorize
  end
  ```

  ### Testing

  * Add mox dependency in order to run controller unit tests

  ```elixir
  {:mox, "~> 0.5", only: :test}
  ```

  * Add following in config/test.exs

  ```elixir
  config :myapp, :oauth_module, Boruta.OauthMock
  config :myapp, :openid_module, Boruta.OpenidMock
  ```

  * Add following in test/test_helper.exs

  ```elixir
  Mox.defmock(Boruta.OauthMock, for: Boruta.OauthModule)
  Mox.defmock(Boruta.OpenidMock, for: Boruta.OpenidModule)
  ```

  ### User flows

  All flows involving resource owners need its integration guided by `Boruta.Oauth.ResourceOwners` behaviour.
  For authorize endpoint, you'll need to assign current_user with a plug and setup login redirections which, with raw setup, raise an error where it is required.
  """

  use Mix.Task

  import Mix.Generator

  @module_paths [
    "controllers/oauth/authorize_controller.ex",
    "controllers/openid/authorize_controller.ex",
    "controllers/openid/jwks_controller.ex",
    "controllers/openid/userinfo_controller.ex",
    "controllers/oauth/introspect_controller.ex",
    "controllers/oauth/revoke_controller.ex",
    "controllers/oauth/token_controller.ex",
    "views/oauth_view.ex",
    "views/openid_view.ex"
  ]

  @raw_file_paths [
    "templates/oauth/error.html.eex"
  ]

  @test_files [
    "unit/oauth/controllers/authorize_controller_test.exs",
    "unit/openid/controllers/authorize_controller_test.exs",
    "unit/openid/controllers/token_controller_test.exs",
    "unit/openid/controllers/jwks_controller_test.exs",
    "unit/openid/controllers/userinfo_controller_test.exs",
    "unit/oauth/controllers/introspect_controller_test.exs",
    "unit/oauth/controllers/revoke_controller_test.exs",
    "unit/oauth/controllers/token_controller_test.exs"
  ]

  def run(_args) do
    if Mix.Project.umbrella?() do
      Mix.raise "mix boruta.gen.controllers must be invoked from within your *_web application root directory"
    end

    otp_app = Mix.Phoenix.context_app()
    web_module = Mix.Phoenix.base() |> Mix.Phoenix.web_module()

    assigns = [
      web_module: Module.split(web_module) |> List.last(),
      otp_app: otp_app
    ]

    copy_modules(otp_app, assigns)
    copy_raw_files(otp_app, assigns)
    copy_test_files(otp_app, assigns)

    IO.puts("""

    * You can add OAuth routes in web application router as follow to expose controller actions

        scope "/oauth", MyAppWeb.Oauth do
          pipe_through :api

          post "/revoke", RevokeController, :revoke
          post "/token", TokenController, :token
          post "/introspect", IntrospectController, :introspect
        end


        scope "/openid", MyAppWeb.Openid do
          pipe_through [:api]

          get "/userinfo", UserinfoController, :userinfo
          post "/userinfo", UserinfoController, :userinfo
          get "/jwks", JwksController, :jwks_index
        end

        ####

        scope "/oauth", MyAppWeb.Oauth do
          pipe_through [:browser, :fetch_current_user]

          get "/authorize", AuthorizeController, :authorize
        end

        ## OR

        scope "/openid", MyAppWeb.Openid do
          pipe_through [:browser, :fetch_current_user]

          get "/authorize", AuthorizeController, :authorize
        end

    ### Testing

    * Add mox dependency in order to run controller unit tests

        {:mox, "~> 0.5", only: :test}

    * Add following in config/test.exs

        config :myapp, :oauth_module, Boruta.OauthMock
        config :myapp, :openid_module, Boruta.OpenidMock

    * Add following in test/test_helper.exs

        Mox.defmock(Boruta.OauthMock, for: Boruta.OauthModule)
        Mox.defmock(Boruta.OpenidMock, for: Boruta.OpenidModule)

    ### User flows

    All flows involving resource owners need its integration guided by `Boruta.Oauth.ResourceOwners` behaviour.
    For authorize endpoint, you'll need to assign current_user with a plug and setup login redirections which, with raw setup, raise an error where it is required.
    """)
  end

  defp copy_modules(otp_app, assigns) do
    List.zip([template_paths(@module_paths), @module_paths])
    |> Enum.map(fn {source, controller_path} ->
      target =
        otp_app
        |> Mix.Phoenix.web_path()
        |> Path.join(controller_path)

      copy_template(source, target, assigns)
    end)
  end

  defp copy_raw_files(otp_app, assigns) do
    List.zip([raw_file_paths(@raw_file_paths), @raw_file_paths])
    |> Enum.map(fn {source, controller_path} ->
      target =
        otp_app
        |> Mix.Phoenix.web_path()
        |> Path.join(controller_path)

      copy_file(source, target, assigns)
    end)
  end

  defp copy_test_files(otp_app, assigns) do
    List.zip([template_paths(@test_files), @test_files])
    |> Enum.map(fn {source, controller_path} ->
      target =
        otp_app
        |> Mix.Phoenix.web_test_path()
        |> Path.join(controller_path)

      copy_template(source, target, assigns)
    end)
  end

  defp template_paths(paths) do
    Enum.map(paths, &template_path/1)
  end

  defp template_path(path) do
    :code.priv_dir(:boruta)
    |> Path.join("templates/boruta.gen.controllers")
    |> Path.join(path <> ".eex")
  end

  defp raw_file_paths(paths) do
    paths
    |> Enum.map(fn path ->
      :code.priv_dir(:boruta)
      |> Path.join("templates/boruta.gen.controllers")
      |> Path.join(path)
    end)
  end
end