lib/auth_web/controllers/role_controller.ex

defmodule AuthWeb.RoleController do
  use AuthWeb, :controller
  alias Auth.Role
  require Logger
  # import Auth.Plugs.IsOwner

  # plug :is_owner when action in [:index]

  def index(conn, _params) do
    # restrict viewing to only roles owned by the person or default roles:
    apps = Auth.App.list_apps(conn)
    app_ids = Enum.map(apps, fn a -> a.id end)
    roles = Role.list_roles_for_apps(app_ids)
    render(conn, "index.html", roles: roles)
  end

  def new(conn, _params) do
    changeset = Role.change_role(%Role{})
    apps = Auth.App.list_apps(conn)
    # Roles Ref/Require Apps: https://github.com/dwyl/auth/issues/112
    # Check if the person already has apps:
    if length(apps) > 0 do
      render(conn, "new.html", changeset: changeset, apps: apps)
    else
      # No apps, instruct them to create an App before Role(s):
      conn
      |> put_flash(:info, "Please create an App before attempting to create Roles")
      |> redirect(to: Routes.app_path(conn, :new))
    end
  end

  def create(conn, %{"role" => role_params}) do
    apps = Auth.App.list_apps(conn)
    # check that the role_params.app_id is owned by the person:
    if person_owns_app?(apps, map_get(role_params, "app_id")) do
      # never allow the request to define the person_id:
      create_attrs = Map.merge(role_params, %{"person_id" => conn.assigns.person.id})

      case Role.create_role(create_attrs) do
        {:ok, role} ->
          conn
          |> put_flash(:info, "Role created successfully.")
          |> redirect(to: Routes.role_path(conn, :show, role))

        {:error, %Ecto.Changeset{} = changeset} ->
          render(conn, "new.html", changeset: changeset, apps: apps)
      end
    else
      # request is attempting to create a role for an app they don't own ...
      changeset = Auth.Role.changeset(%Role{}, role_params)

      conn
      |> put_status(:not_found)
      |> put_flash(:info, "Please select an app you own.")
      |> render("new.html", changeset: changeset, apps: apps)
    end
  end

  def show(conn, %{"id" => id}) do
    role = Role.get_role!(id, conn.assigns.person.id)

    if is_nil(role) do
      AuthWeb.AuthController.not_found(conn, "role not found.")
    else
      render(conn, "show.html", role: role)
    end
  end

  def edit(conn, %{"id" => id}) do
    role = Role.get_role!(id, conn.assigns.person.id)

    if is_nil(role) do
      AuthWeb.AuthController.not_found(conn, "role not found.")
    else
      changeset = Role.change_role(role)
      apps = Auth.App.list_apps(conn)
      render(conn, "edit.html", role: role, changeset: changeset, apps: apps)
    end
  end

  def update(conn, %{"id" => id, "role" => role_params}) do
    role = Role.get_role!(id, conn.assigns.person.id)
    apps = Auth.App.list_apps(conn)
    # cannot update a role that doesn't exist (or they don't own):
    if is_nil(role) do
      AuthWeb.AuthController.not_found(conn, "role not found.")
    else
      # confirm that the person owns the app they are attempting to attach a role to:
      if person_owns_app?(apps, map_get(role_params, "app_id")) do
        case Role.update_role(role, role_params) do
          {:ok, role} ->
            conn
            |> put_flash(:info, "Role updated successfully.")
            |> redirect(to: Routes.role_path(conn, :show, role))

          {:error, %Ecto.Changeset{} = changeset} ->
            apps = Auth.App.list_apps(conn)
            render(conn, "edit.html", role: role, changeset: changeset, apps: apps)
        end
      else
        AuthWeb.AuthController.not_found(conn, "App not found.")
      end
    end
  end

  # https://elixirforum.com/t/map-key-is-a-atom-or-string/13285/2
  #  our use-case for this is specific keys in controller params
  # mix gen creates tests with atom keys whereas controller expect string keys!
  defp map_get(map, string_key, default \\ 0) do
    to_string(
      Map.get(map, string_key) ||
        Map.get(map, String.to_atom(string_key), default)
    )
  end

  #  confirm that the person owns the app they want add a role for:
  defp person_owns_app?(apps, app_id) do
    app_ids = Enum.map(apps, fn a -> to_string(a.id) end)
    Enum.member?(app_ids, app_id)
  end

  def delete(conn, %{"id" => id}) do
    # can only delete a role you own:
    role = Role.get_role!(id, conn.assigns.person.id)

    if is_nil(role) do
      AuthWeb.AuthController.not_found(conn, "role not found.")
    else
      {:ok, _role} = Role.delete_role(role)

      conn
      |> put_flash(:info, "Role deleted successfully.")
      |> redirect(to: Routes.role_path(conn, :index))
    end
  end

  @doc """
  grant_role/3 grants a role to the given person
  the conn must have conn.assigns.person to check for admin in order to grant the role.
  grantee_id should be a valid person.id (the person you want to grant the role to) and
  role_id a valid role.id
  """
  def grant(conn, params) do
    # confirm that the granter is either superadmin (conn.assigns.person.id == 1)
    # or has an "admin" role (1 || 2)
    granter_id = conn.assigns.person.id
    apps = Auth.App.list_apps(conn)
    # app_ids_list = Enum.map(apps, fn a -> a.id end)
    # role list includes default_roles 1-8 and any custom roles
    role_id = map_get(params, "role_id")
    app_id = map_get(params, "app_id")

    if person_owns_app?(apps, app_id) and app_owns_role?(app_id, role_id) do
      grantee_id = map_get(params, "person_id")
      Auth.PeopleRoles.insert(app_id, grantee_id, granter_id, role_id)
      redirect(conn, to: Routes.people_path(conn, :show, grantee_id))
    else
      Logger.error("person.id #{granter_id} attempted to grant role.id #{role_id}")
      AuthWeb.AuthController.unauthorized(conn)
    end
  end

  defp app_owns_role?(app_id, role_id) do
    role_list_ids = Auth.Role.list_role_ids_for_app(app_id)
    Enum.member?(role_list_ids, role_id)
  end

  @doc """
  revoke/2 revokes a role
  """
  def revoke(conn, params) do
    # confirm that the granter is either superadmin (conn.assigns.person.id == 1)
    # or has an "admin" role (1 || 2)
    if conn.assigns.person.id == 1 do
      people_roles_id = map_get(params, "people_roles_id")
      pr = Auth.PeopleRoles.get_by_id(people_roles_id)

      if conn.method == "GET" do
        render(conn, "revoke.html",
          role: pr,
          people_roles_id: people_roles_id,
          apps: Auth.App.list_apps(conn)
        )
      else
        Auth.PeopleRoles.revoke(conn.assigns.person.id, people_roles_id)
        redirect(conn, to: Routes.people_path(conn, :show, pr.person_id))
      end
    else
      AuthWeb.AuthController.unauthorized(conn)
    end
  end
end