defmodule GitGudWeb.Api.Mobile.Helpers do
@moduledoc """
Shared plumbing for the mobile API controllers: repo resolution
behind the viewer's visibility rules, uniform error bodies, and the
small amount of param coercion every listing endpoint repeats.
`fetch_repo/4` folds "does it exist" and "may this token's owner see
it" into one answer on purpose — a private repo the caller can't read
must 404, not 403, or the API leaks which private repos exist.
"""
import Plug.Conn
import Phoenix.Controller, only: [json: 2]
alias GitGud.Organizations
alias GitGud.Repositories
@doc """
Resolve `owner/name` and check `action` against the viewer's effective
permission. `{:ok, repo}` or `{:error, halted_conn}`.
"""
def fetch_repo(conn, user, owner, name, action \\ :read) do
case Repositories.find_by_path(owner, name) do
nil ->
{:error, send_error(conn, 404, "repository not found")}
# A job's token reaches its own repository and no other — its
# permissions, not the CI account's, say what it may do there.
repo when is_map_key(conn.assigns, :api_job) ->
if repo.id == conn.assigns.api_job["pid"],
do: {:ok, GitGud.Repo.preload(repo, [:owner, :organization])},
else: {:error, send_error(conn, 404, "repository not found")}
repo ->
cond do
Organizations.can?(user, repo, action) ->
{:ok, GitGud.Repo.preload(repo, [:owner, :organization])}
# Readable but not writable: say so, since the caller already
# knows the repo exists.
Organizations.can?(user, repo, :read) ->
{:error, send_error(conn, 403, "requires #{action} access to #{owner}/#{name}")}
true ->
{:error, send_error(conn, 404, "repository not found")}
end
end
end
@doc "A `:state` filter param, restricted to what the contexts accept."
def state_param(params, default \\ "open") do
case params["state"] do
s when s in ["open", "closed", "merged", "all"] -> s
_ -> default
end
end
@doc "A positive integer param, clamped to `max`."
def int_param(params, key, default, max) do
case Integer.parse(to_string(params[key] || "")) do
{n, _} when n > 0 -> min(n, max)
_ -> default
end
end
@doc "A `:number` path segment, or a halted 404 if it isn't one."
def number_param(conn, raw) do
case Integer.parse(to_string(raw)) do
{n, ""} when n > 0 -> {:ok, n}
_ -> {:error, send_error(conn, 404, "not found")}
end
end
@doc """
`:ok`, or a halted 429 when a job's run has already opened as many
issues or pull requests (`policy`, a `GitGud.RateLimit` policy keyed
by run) as one run may. People aren't counted here.
"""
def job_quota(conn, policy) do
case conn.assigns[:api_job] do
%{"rid" => rid} ->
case GitGud.RateLimit.hit(policy, rid) do
:ok ->
:ok
{:error, _retry_after} ->
{limit, _} = GitGud.RateLimit.limits(policy)
{:error,
send_error(conn, 429, "this run has already opened #{limit}; that's the limit")}
end
_ ->
:ok
end
end
@doc "The run a job token belongs to, as `workflow_run_id:` for what it creates."
def job_opts(conn) do
case conn.assigns[:api_job] do
%{"rid" => rid} -> [workflow_run_id: rid]
_ -> []
end
end
def send_error(conn, status, message) do
conn |> put_status(status) |> json(%{"message" => message}) |> halt()
end
@doc """
Turn a changeset into the flat `field message; field message` string
the API returns — the phone shows it in a snackbar, so structure
buys nothing.
"""
def changeset_message(%Ecto.Changeset{} = cs) do
cs
|> Ecto.Changeset.traverse_errors(fn {m, _} -> m end)
|> Enum.map_join("; ", fn {k, v} -> "#{k} #{Enum.join(v, ", ")}" end)
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
3.8 KiB · text
History
b868907