1.9 KiB · text History 4b2ca85
defmodule GitGud.Avatars.Avatar do
  @moduledoc """
  An uploaded avatar for a user, organization or repository.

  Polymorphic on `{owner_type, owner_id}`, the same shape `Labels`
  uses — the three owners want identical fields, and one table means
  the serving path and the settings form exist once rather than three
  times.

  The bytes are in `GitGud.Storage`; this row is the pointer plus what
  a response needs without fetching them.
  """
  use Ecto.Schema

  import Ecto.Changeset

  @owner_types ~w(user organization repository)

  # What a browser will actually render, and nothing that can carry
  # script. SVG is deliberately absent: it is a document format with
  # script and external references in it, and serving one from the
  # forge's own origin would be a stored XSS with extra steps.
  @content_types ~w(image/png image/jpeg image/gif image/webp)

  schema "avatars" do
    field :owner_type, :string
    field :owner_id, :integer

    field :storage_bucket, :string
    field :storage_key, :string
    field :content_type, :string
    field :size, :integer, default: 0
    field :sha256, :string

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(avatar, attrs) do
    avatar
    |> cast(attrs, [
      :owner_type,
      :owner_id,
      :storage_bucket,
      :storage_key,
      :content_type,
      :size,
      :sha256
    ])
    |> validate_required([
      :owner_type,
      :owner_id,
      :storage_bucket,
      :storage_key,
      :content_type,
      :sha256
    ])
    |> validate_inclusion(:owner_type, @owner_types)
    |> validate_inclusion(:content_type, @content_types,
      message: "must be a PNG, JPEG, GIF or WebP image"
    )
    |> unique_constraint([:owner_type, :owner_id])
  end

  @doc "Owner kinds an avatar can belong to."
  def owner_types, do: @owner_types

  @doc "Image types accepted on upload."
  def content_types, do: @content_types
end