- LIKE wildcard injection: sanitize %, _ in search queries (devices, sites, gaiia) - Jason.decode! → Jason.decode with error handling for untrusted input - inspect() leak: replace with generic error messages, log details server-side - SSRF protection: URL validator blocks private IPs, localhost, non-HTTP schemes - SSRF validation added to HTTP monitoring executor and integration credentials - GraphQL complexity limits: always applied, not just in prod - GraphQL introspection: also check GET query params, not just body - Stripe webhook: explicit nil/empty checks for signature and body - Cookie security: secure flag for session (prod), http_only+secure for remember_me - Honeybadger API key: read from env var with fallback - String.to_integer → Integer.parse with fallback for URL params - String.to_atom → whitelist map for HTTP methods - Gaiia webhook: remove secret_len and expected signature from log - Admin API: add rate limiting pipeline - to_atom_keys: per-key fallback instead of all-or-nothing rescue
6.1 KiB
6.1 KiB
Ecto Context Module Template
Copy-pasteable template for a new Ecto schema + context + test following TowerOps conventions.
Replace __CONTEXT__ with PascalCase context name (e.g., Schedules).
Replace __SCHEMA__ with PascalCase schema name (e.g., Schedule).
Replace __resource__ with snake_case (e.g., schedule).
Replace __resources__ with plural snake_case (e.g., schedules).
Replace __table__ with the database table name (e.g., schedules).
lib/towerops/__context__/__resource__.ex (Schema)
defmodule Towerops.__CONTEXT__.__SCHEMA__ do
@moduledoc """
Schema for __resources__.
Describe what this resource represents and its relationships.
"""
use Ecto.Schema
import Ecto.Changeset
alias Towerops.Organizations.Organization
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "__table__" do
field :name, :string
field :description, :string
field :enabled, :boolean, default: true
belongs_to :organization, Organization
timestamps()
end
@required_fields ~w(name organization_id)a
@optional_fields ~w(description enabled)a
def changeset(__resource__, attrs) do
__resource__
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> validate_length(:name, max: 255)
|> validate_length(:description, max: 1000)
|> foreign_key_constraint(:organization_id)
end
end
lib/towerops/__context__.ex (Context)
defmodule Towerops.__CONTEXT__ do
@moduledoc """
Context for managing __resources__.
"""
import Ecto.Query
alias Towerops.__CONTEXT__.__SCHEMA__
alias Towerops.Repo
@preloads [:organization]
def create___resource__(attrs) do
%__SCHEMA__{}
|> __SCHEMA__.changeset(attrs)
|> Repo.insert()
end
def update___resource__(%__SCHEMA__{} = __resource__, attrs) do
__resource__
|> __SCHEMA__.changeset(attrs)
|> Repo.update()
end
def delete___resource__(%__SCHEMA__{} = __resource__) do
Repo.delete(__resource__)
end
def get___resource__!(id) do
__SCHEMA__
|> Repo.get!(id)
|> Repo.preload(@preloads)
end
def list___resources__(organization_id, opts \\ []) do
__SCHEMA__
|> where([r], r.organization_id == ^organization_id)
|> apply_filter(opts[:filter])
|> order_by([r], desc: r.inserted_at)
|> Repo.all()
|> Repo.preload(@preloads)
end
defp apply_filter(query, :active) do
where(query, [r], r.enabled == true)
end
defp apply_filter(query, :archived) do
where(query, [r], r.enabled == false)
end
defp apply_filter(query, _), do: query
end
Migration
defmodule Towerops.Repo.Migrations.Create__SCHEMA__s do
use Ecto.Migration
def change do
create table(:__table__, primary_key: false) do
add :id, :binary_id, primary_key: true
add :name, :string, null: false
add :description, :text
add :enabled, :boolean, default: true, null: false
add :organization_id, references(:organizations, type: :binary_id, on_delete: :delete_all),
null: false
timestamps()
end
create index(:__table__, [:organization_id])
end
end
test/towerops/__context___test.exs (Context Tests)
defmodule Towerops.__CONTEXT__Test do
use Towerops.DataCase
alias Towerops.__CONTEXT__
alias Towerops.__CONTEXT__.__SCHEMA__
setup do
user = insert_user()
{:ok, organization} =
Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
%{organization: organization, user: user}
end
describe "create___resource__/1" do
test "creates with valid attrs", %{organization: org} do
attrs = %{
name: "Test __RESOURCE_TITLE__",
organization_id: org.id
}
assert {:ok, %__SCHEMA__{} = __resource__} = __CONTEXT__.create___resource__(attrs)
assert __resource__.name == "Test __RESOURCE_TITLE__"
assert __resource__.organization_id == org.id
end
test "fails without required fields" do
assert {:error, changeset} = __CONTEXT__.create___resource__(%{})
assert errors_on(changeset) |> Map.has_key?(:name)
assert errors_on(changeset) |> Map.has_key?(:organization_id)
end
end
describe "update___resource__/2" do
test "updates with valid attrs", %{organization: org} do
{:ok, __resource__} =
__CONTEXT__.create___resource__(%{name: "Original", organization_id: org.id})
assert {:ok, updated} = __CONTEXT__.update___resource__(__resource__, %{name: "Updated"})
assert updated.name == "Updated"
end
end
describe "delete___resource__/1" do
test "deletes the resource", %{organization: org} do
{:ok, __resource__} =
__CONTEXT__.create___resource__(%{name: "To Delete", organization_id: org.id})
assert {:ok, _} = __CONTEXT__.delete___resource__(__resource__)
assert_raise Ecto.NoResultsError, fn -> __CONTEXT__.get___resource__!(__resource__.id) end
end
end
describe "list___resources__/2" do
test "lists resources for organization", %{organization: org} do
{:ok, _} = __CONTEXT__.create___resource__(%{name: "Item 1", organization_id: org.id})
{:ok, _} = __CONTEXT__.create___resource__(%{name: "Item 2", organization_id: org.id})
__resources__ = __CONTEXT__.list___resources__(org.id)
assert length(__resources__) == 2
end
test "filters by active status", %{organization: org} do
{:ok, _} = __CONTEXT__.create___resource__(%{name: "Active", organization_id: org.id, enabled: true})
{:ok, _} = __CONTEXT__.create___resource__(%{name: "Archived", organization_id: org.id, enabled: false})
active = __CONTEXT__.list___resources__(org.id, filter: :active)
assert length(active) == 1
assert hd(active).name == "Active"
end
end
describe "get___resource__!/1" do
test "returns the resource with preloads", %{organization: org} do
{:ok, created} = __CONTEXT__.create___resource__(%{name: "Find Me", organization_id: org.id})
__resource__ = __CONTEXT__.get___resource__!(created.id)
assert __resource__.name == "Find Me"
assert __resource__.organization.id == org.id
end
end
end