cover/Elixir.DaProductApp.Audit.ApiRequestLog.html

1 defmodule DaProductApp.Audit.ApiRequestLog do
2 @moduledoc """
3 Schema for API request/response logging to support audit requirements.
4
5 This table captures all API interactions for compliance, debugging,
6 and performance monitoring.
7 """
8 use Ecto.Schema
9 import Ecto.Changeset
10
11 alias DaProductApp.Accounts.User
12 alias DaProductApp.Partners.Partner
13
14
:-(
schema "api_request_logs" do
15 field :request_id, :string
16 field :method, :string
17 field :path, :string
18 field :query_params, :string
19 field :headers, :string
20 field :body, :string
21 field :response_status, :integer
22 field :response_body, :string
23 field :response_headers, :string
24 field :duration_ms, :integer
25 field :ip_address, :string
26 field :user_agent, :string
27 field :error_details, :string
28
29 # Relationships
30 belongs_to :user, User
31 belongs_to :partner, Partner
32
33 timestamps(type: :utc_datetime)
34 end
35
36 @required_fields ~w(request_id method path)a
37 @optional_fields ~w(
38 query_params headers body response_status response_body response_headers
39 duration_ms ip_address user_agent user_id partner_id error_details
40 )a
41
42 def changeset(log, attrs) do
43 log
44 |> cast(attrs, @required_fields ++ @optional_fields)
45 |> validate_required(@required_fields)
46 |> validate_inclusion(:method, ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"])
47 |> validate_number(:response_status, greater_than_or_equal_to: 100, less_than: 600)
48 |> validate_number(:duration_ms, greater_than_or_equal_to: 0)
49 |> foreign_key_constraint(:user_id)
50
:-(
|> foreign_key_constraint(:partner_id)
51 end
52
53 def create_changeset(attrs) do
54 %__MODULE__{}
55 |> changeset(attrs)
56
:-(
|> put_change(:request_id, generate_request_id())
57 end
58
59 @doc """
60 Sanitize sensitive data from request/response bodies before logging.
61 """
62 def sanitize_body(body) when is_binary(body) do
63 # Remove sensitive fields like passwords, tokens, etc.
64
:-(
sensitive_patterns = [
65 ~r/"password"\s*:\s*"[^"]*"/i,
66 ~r/"token"\s*:\s*"[^"]*"/i,
67 ~r/"api_key"\s*:\s*"[^"]*"/i,
68 ~r/"secret"\s*:\s*"[^"]*"/i,
69 ~r/"authorization"\s*:\s*"[^"]*"/i
70 ]
71
72
:-(
Enum.reduce(sensitive_patterns, body, fn pattern, acc ->
73
:-(
Regex.replace(pattern, acc, fn match ->
74
:-(
String.replace(match, ~r/"[^"]*"$/, "\"[REDACTED]\"")
75 end)
76 end)
77 end
78
:-(
def sanitize_body(body), do: body
79
80 @doc """
81 Sanitize sensitive headers before logging.
82 """
83 def sanitize_headers(headers) when is_map(headers) do
84
:-(
sensitive_headers = ~w(authorization x-api-key x-auth-token cookie)
85
86
:-(
Enum.reduce(sensitive_headers, headers, fn header, acc ->
87
:-(
Map.update(acc, header, nil, fn _ -> "[REDACTED]" end)
88 end)
89 end
90
:-(
def sanitize_headers(headers), do: headers
91
92 defp generate_request_id do
93
:-(
"REQ" <>
94
:-(
(System.system_time(:millisecond) |> to_string()) <>
95
:-(
(:rand.uniform(99999) |> to_string() |> String.pad_leading(5, "0"))
96 end
97 end
Line Hits Source