3.3 KiB · text History b868907
defmodule GitGudWeb.RepoAdmin do
  @moduledoc """
  Authorization for the repository pages that change things: settings
  pages (repo admin) and the wiki editor (write access, not banned).

  `RepoAuth :require_read` is the only gate the router puts in front of
  these pages, and any signed-in user can read a public or internal
  repository. So each page checks in `mount/3` and — because a client
  can push any event with any params whatever the page rendered —
  again in every handler that changes something. The handler check
  re-reads the repository and the user's rights from the database
  rather than trusting an assign made at mount: a socket can outlive
  the admin role it was opened with.
  """

  use GitGudWeb, :verified_routes

  import Phoenix.LiveView, only: [put_flash: 3, redirect: 2]

  alias GitGud.Moderation
  alias GitGud.Organizations
  alias GitGud.Repositories.Repository
  alias GitGud.Repositories.Storage
  alias GitGudWeb.RepoLive.Header

  @doc "Whether `user` administers `repo` (see `RepoLive.Header.admin?/2`)."
  def admin?(user, repo), do: Header.admin?(user, repo)

  @doc """
  Whether `user` may write to `repo` — at least `:write` and not banned
  from it. What the wiki editor requires.
  """
  def writer?(nil, _repo), do: false
  def writer?(_user, nil), do: false

  def writer?(user, %Repository{} = repo),
    do: !!Organizations.can?(user, repo, :write) and not Moderation.banned?(repo, user)

  @doc "`admin?/2` for the socket's user, against a fresh copy of its `:repo`."
  def admin_now?(socket), do: admin?(user(socket), fresh_repo(socket))

  @doc "`writer?/2` for the socket's user, against a fresh copy of its `:repo`."
  def writer_now?(socket), do: writer?(user(socket), fresh_repo(socket))

  @doc """
  Runs `fun` (a handler body returning `{:noreply, socket}`) only if the
  socket's user still administers the repository; otherwise flashes and
  changes nothing.
  """
  def with_admin(socket, fun) when is_function(fun, 0) do
    if admin_now?(socket),
      do: fun.(),
      else:
        {:noreply, put_flash(socket, :error, "You don't have access to manage this repository.")}
  end

  @doc "Like `with_admin/2`, for write access."
  def with_writer(socket, fun) when is_function(fun, 0) do
    if writer_now?(socket),
      do: fun.(),
      else: {:noreply, put_flash(socket, :error, "You can't edit this repository.")}
  end

  @doc "Where a mount sends someone who isn't allowed in: the repository's page."
  def deny(socket, %Repository{} = repo, message \\ nil) do
    socket
    |> put_flash(:error, message || "You don't have access to manage this repository.")
    |> redirect(to: ~p"/r/#{Storage.repo_handle(repo)}/#{repo.name}")
  end

  @doc """
  Parses an id pushed by the client. Anything but a positive integer is
  `nil`, so a junk id finds nothing instead of crashing the view.
  """
  def parse_id(id) when is_integer(id) and id > 0, do: id

  def parse_id(id) when is_binary(id) do
    case Integer.parse(id) do
      {n, ""} when n > 0 -> n
      _ -> nil
    end
  end

  def parse_id(_), do: nil

  defp user(socket), do: socket.assigns[:current_scope] && socket.assigns.current_scope.user

  defp fresh_repo(%{assigns: %{repo: %Repository{id: id}}}) do
    case GitGud.Repo.get(Repository, id) do
      nil -> nil
      repo -> GitGud.Repo.preload(repo, :organization)
    end
  end

  defp fresh_repo(_), do: nil
end