Current section
Files
Jump to
Current section
Files
lib/mix/tasks/chronicle.install.ex
defmodule Mix.Tasks.Chronicle.Install do
use Mix.Task
@shortdoc "Generates the Ecto audit migration and runtime configuration example"
@moduledoc """
Generates a host migration and `config/audit.runtime.exs.example`.
mix chronicle.install --repo MyApp.Repo
mix chronicle.install --repo MyApp.Repo --migrations-path priv/custom/migrations
Existing files are never overwritten.
"""
@switches [repo: :string, migrations_path: :string, phoenix: :boolean]
@aliases [r: :repo]
@impl true
def run(args) do
{opts, positional, invalid} = OptionParser.parse(args, strict: @switches, aliases: @aliases)
if positional != [] or invalid != [] do
Mix.raise("invalid arguments; run `mix help chronicle.install`")
end
repo = Keyword.get(opts, :repo) || Mix.raise("--repo is required")
validate_module!(repo)
migrations_path = Keyword.get(opts, :migrations_path, "priv/repo/migrations")
timestamp = Calendar.strftime(DateTime.utc_now(), "%Y%m%d%H%M%S")
migration_path = Path.join(migrations_path, "#{timestamp}_install_audit.exs")
config_path = "config/audit.runtime.exs.example"
write_new!(migration_path, migration(repo))
write_new!(config_path, runtime_config(repo))
Mix.shell().info("Generated #{migration_path}")
Mix.shell().info("Generated #{config_path}")
Mix.shell().info("Run your repository migration command.")
if Keyword.get(opts, :phoenix, false) do
Mix.shell().info("")
Mix.shell().info("Add this after authentication in the relevant Phoenix pipeline:")
Mix.shell().info("plug Chronicle.Phoenix.Plug, actor_assign: :current_user")
end
Mix.shell().info("")
Mix.shell().info("Fresh 256-bit development key (not written to disk):")
Mix.shell().info(Chronicle.Keys.generate())
end
defp migration(repo) do
"""
defmodule #{repo}.Migrations.InstallAudit do
use Ecto.Migration
def up, do: Chronicle.Ecto.Migration.up()
def down, do: Chronicle.Ecto.Migration.down()
end
"""
end
defp runtime_config(repo) do
"""
import Config
config :chronicle,
repo: #{repo},
signing_key: {:system, "CHRONICLE_SIGNING_KEY_BASE64", :base64}
# Before rotating keys, move to the explicit keyring configuration
# documented under "Key rotation and operations".
#
# For rollback detection, implement an external Chronicle.CheckpointStore
# and add this to your supervision tree:
#
# {Chronicle.Verifier, name: MyApp.AuditVerifier, store: :primary}
"""
end
defp write_new!(path, contents) do
if File.exists?(path), do: Mix.raise("refusing to overwrite existing file #{path}")
File.mkdir_p!(Path.dirname(path))
File.write!(path, contents, [:exclusive])
end
defp validate_module!(module) do
unless Regex.match?(~r/^[A-Z][A-Za-z0-9_]*(\.[A-Z][A-Za-z0-9_]*)*$/, module) do
Mix.raise("--repo must be an Elixir module name, got: #{inspect(module)}")
end
end
end