From c217fb6203a3a14bec0c538840440fd419e50e70 Mon Sep 17 00:00:00 2001 From: Tsvetozar Penov Date: Wed, 30 Oct 2024 15:49:30 +0100 Subject: [PATCH] Add audit log --- config/config.exs | 8 +++ lib/sanbase/repo.ex | 1 + lib/sanbase/version.ex | 34 ++++++++++ lib/sanbase_web/generic_admin/version.ex | 47 +++++++++++++ mix.exs | 3 +- mix.lock | 1 + .../20241030141825_add_ex_audit_versions.exs | 32 +++++++++ priv/repo/structure.sql | 66 +++++++++++++++++++ 8 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 lib/sanbase/version.ex create mode 100644 lib/sanbase_web/generic_admin/version.ex create mode 100644 priv/repo/migrations/20241030141825_add_ex_audit_versions.exs diff --git a/config/config.exs b/config/config.exs index 4883b8a60..753bb3c7d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -284,6 +284,14 @@ config :nostrum, :message_content ] +config :ex_audit, + ecto_repos: [Sanbase.Repo], + version_schema: Sanbase.Version, + tracked_schemas: [ + Sanbase.Accounts.User + ], + primitive_structs: [DateTime, NaiveDateTime, Date] + # Import configs import_config "ueberauth_config.exs" import_config "scrapers_config.exs" diff --git a/lib/sanbase/repo.ex b/lib/sanbase/repo.ex index 52aae1974..c867f71e8 100644 --- a/lib/sanbase/repo.ex +++ b/lib/sanbase/repo.ex @@ -1,5 +1,6 @@ defmodule Sanbase.Repo do use Ecto.Repo, otp_app: :sanbase, adapter: Ecto.Adapters.Postgres + use ExAudit.Repo alias Sanbase.Utils.Config diff --git a/lib/sanbase/version.ex b/lib/sanbase/version.ex new file mode 100644 index 000000000..1801362ec --- /dev/null +++ b/lib/sanbase/version.ex @@ -0,0 +1,34 @@ +defmodule Sanbase.Version do + use Ecto.Schema + import Ecto.Changeset + + schema "versions" do + # The patch in Erlang External Term Format + field(:patch, ExAudit.Type.Patch) + + # supports UUID and other types as well + field(:entity_id, :integer) + + # name of the table the entity is in + field(:entity_schema, ExAudit.Type.Schema) + + # type of the action that has happened to the entity (created, updated, deleted) + field(:action, ExAudit.Type.Action) + + # when has this happened + field(:recorded_at, :utc_datetime) + + # was this change part of a rollback? + field(:rollback, :boolean, default: false) + + # custom fields + belongs_to(:user, Sanbase.Accounts.User) + end + + def changeset(struct, params \\ %{}) do + struct + |> cast(params, [:patch, :entity_id, :entity_schema, :action, :recorded_at, :rollback]) + # custom fields + |> cast(params, [:user_id]) + end +end diff --git a/lib/sanbase_web/generic_admin/version.ex b/lib/sanbase_web/generic_admin/version.ex new file mode 100644 index 000000000..2e7af8ec2 --- /dev/null +++ b/lib/sanbase_web/generic_admin/version.ex @@ -0,0 +1,47 @@ +defmodule SanbaseWeb.GenericAdmin.Version do + import Ecto.Query + def schema_module, do: Sanbase.Version + + def resource() do + %{ + actions: [:show], + preloads: [:user], + index_fields: [ + :id, + :entity_id, + :entity_schema, + :action, + :recorded_at, + :rollback + ], + fields_override: %{ + patch: %{ + value_modifier: &format_patch/1 + } + } + } + end + + defp format_patch(%{patch: patch}) when is_map(patch) do + patch + |> Enum.map_join("\n", fn {field, change} -> + format_change(field, change) + end) + end + + defp format_patch(_), do: "" + + defp format_change(field, {:changed, {:primitive_change, old_val, new_val}}) do + "#{field}: #{inspect(old_val)} → #{inspect(new_val)}" + end + + defp format_change(field, {:changed, nested}) when is_map(nested) do + nested_changes = + nested + |> Enum.map_join(", ", fn {k, v} -> format_change(k, v) end) + + "#{field}: {#{nested_changes}}" + end + + defp format_change(field, other), do: "#{field}: #{inspect(other)}" +end diff --git a/mix.exs b/mix.exs index 3ea61c354..7800859bb 100644 --- a/mix.exs +++ b/mix.exs @@ -148,7 +148,8 @@ defmodule Sanbase.Mixfile do {:vex, "~> 0.9", override: true}, {:waffle, "~> 1.1"}, {:websockex, "~> 0.4.3"}, - {:mox, "~> 1.2"} + {:mox, "~> 1.2"}, + {:ex_audit, "~> 0.10.0"} ] end diff --git a/mix.lock b/mix.lock index a15d527e4..6045f0ad0 100644 --- a/mix.lock +++ b/mix.lock @@ -40,6 +40,7 @@ "ethereumex": {:hex, :ethereumex, "0.10.6", "6d75cac39b5b7a720b064fe48563f205d3d9784e5bde25f983dd07cf306c2a6d", [:make, :mix], [{:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "58cf926239dabf8bd1fc6cf50a37b926274240b7f58ba5b235a20b5500a9a7e1"}, "event_bus": {:hex, :event_bus, "1.7.0", "29a36fc09e8c4463c82206b6a300fa1d61cf4baf9a7b4e7cf0c3efb99c73998e", [:mix], [], "hexpm", "e556470f49f53060a0696c4bad81341252685011afc69eda25032c8a3a86eb2e"}, "ex_abi": {:hex, :ex_abi, "0.8.0", "bb08827bd8d71dbb311c69ac55a008669dfabe2ce5b58d65f97c08c0aba60ec6", [:mix], [{:ex_keccak, "~> 0.7.5", [hex: :ex_keccak, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "bbdae12c186aeeb4c53dd7c7c57f457923602db315aa1f66d7427467c8ad77af"}, + "ex_audit": {:hex, :ex_audit, "0.10.0", "98625c2c3a54950cac85eb1f69caa919638c68fb65f8d4951d3d145ab52782e4", [:mix], [{:ecto, ">= 3.8.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, ">= 3.8.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "9ec37f6a9ed986aea37eb311e1dd0b0c9dd855f0dd9a107837276db864e4c69b"}, "ex_aws": {:hex, :ex_aws, "2.5.4", "86c5bb870a49e0ab6f5aa5dd58cf505f09d2624ebe17530db3c1b61c88a673af", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e82bd0091bb9a5bb190139599f922ff3fc7aebcca4374d65c99c4e23aa6d1625"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, "ex_json_schema": {:hex, :ex_json_schema, "0.10.2", "7c4b8c1481fdeb1741e2ce66223976edfb9bccebc8014f6aec35d4efe964fb71", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "37f43be60f8407659d4d0155a7e45e7f406dab1f827051d3d35858a709baf6a6"}, diff --git a/priv/repo/migrations/20241030141825_add_ex_audit_versions.exs b/priv/repo/migrations/20241030141825_add_ex_audit_versions.exs new file mode 100644 index 000000000..0c5e59cc5 --- /dev/null +++ b/priv/repo/migrations/20241030141825_add_ex_audit_versions.exs @@ -0,0 +1,32 @@ +defmodule Sanbase.Repo.Migrations.AddExAuditVersions do + use Ecto.Migration + + def change do + create table(:versions) do + # The patch in Erlang External Term Format + add(:patch, :binary) + + # supports UUID and other types as well + add(:entity_id, :integer) + + # name of the table the entity is in + add(:entity_schema, :string) + + # type of the action that has happened to the entity (created, updated, deleted) + add(:action, :string) + + # when has this happened + add(:recorded_at, :utc_datetime) + + # was this change part of a rollback? + add(:rollback, :boolean, default: false) + + # optional fields that you can define yourself + # for example, it's a good idea to track who did the change + add(:user_id, references(:users, on_update: :update_all, on_delete: :nilify_all)) + end + + # create this if you are going to have more than a hundred of thousands of versions + create(index(:versions, [:entity_schema, :entity_id])) + end +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 9fc210110..32344b0e6 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -4528,6 +4528,41 @@ CREATE SEQUENCE public.users_id_seq ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; +-- +-- Name: versions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.versions ( + id bigint NOT NULL, + patch bytea, + entity_id integer, + entity_schema character varying(255), + action character varying(255), + recorded_at timestamp(0) without time zone, + rollback boolean DEFAULT false, + user_id bigint +); + + +-- +-- Name: versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.versions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.versions_id_seq OWNED BY public.versions.id; + + -- -- Name: votes_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -5589,6 +5624,13 @@ ALTER TABLE ONLY public.user_uniswap_staking ALTER COLUMN id SET DEFAULT nextval ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); +-- +-- Name: versions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.versions ALTER COLUMN id SET DEFAULT nextval('public.versions_id_seq'::regclass); + + -- -- Name: votes id; Type: DEFAULT; Schema: public; Owner: - -- @@ -6620,6 +6662,14 @@ ALTER TABLE ONLY public.users ADD CONSTRAINT users_pkey PRIMARY KEY (id); +-- +-- Name: versions versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.versions + ADD CONSTRAINT versions_pkey PRIMARY KEY (id); + + -- -- Name: votes votes_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -7708,6 +7758,13 @@ CREATE UNIQUE INDEX users_twitter_id_index ON public.users USING btree (twitter_ CREATE UNIQUE INDEX users_username_index ON public.users USING btree (username); +-- +-- Name: versions_entity_schema_entity_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX versions_entity_schema_entity_id_index ON public.versions USING btree (entity_schema, entity_id); + + -- -- Name: votes_chart_configuration_id_user_id_index; Type: INDEX; Schema: public; Owner: - -- @@ -8918,6 +8975,14 @@ ALTER TABLE ONLY public.user_uniswap_staking ADD CONSTRAINT user_uniswap_staking_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; +-- +-- Name: versions versions_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.versions + ADD CONSTRAINT versions_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; + + -- -- Name: votes votes_chart_configuration_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -9539,3 +9604,4 @@ INSERT INTO public."schema_migrations" (version) VALUES (20241018075640); INSERT INTO public."schema_migrations" (version) VALUES (20241029080754); INSERT INTO public."schema_migrations" (version) VALUES (20241029082533); INSERT INTO public."schema_migrations" (version) VALUES (20241029151959); +INSERT INTO public."schema_migrations" (version) VALUES (20241030141825);