cover/Elixir.DaProductApp.Partners.html

1 defmodule DaProductApp.Partners do
2 @moduledoc """
3 The Partners context for managing partner authentication and operations.
4 """
5
6 import Ecto.Query, warn: false
7 alias DaProductApp.Repo
8 alias DaProductApp.Partners.Partner
9
10 @doc """
11 Returns the list of partners.
12 """
13 def list_partners do
14
:-(
Repo.all(Partner)
15 end
16
17 @doc """
18 Gets a single partner.
19 """
20
:-(
def get_partner!(id), do: Repo.get!(Partner, id)
21
22 @doc """
23 Gets a partner by ID, returns {:ok, partner} or {:error, :not_found}
24 """
25 def get_partner(id) do
26
:-(
case Repo.get(Partner, id) do
27
:-(
nil -> {:error, :not_found}
28
:-(
partner -> {:ok, partner}
29 end
30 end
31
32 @doc """
33 Gets a partner by partner_code.
34 """
35 def get_partner_by_code(partner_code) do
36
:-(
Repo.get_by(Partner, partner_code: partner_code)
37 end
38
39 @doc """
40 Gets a partner by API key for authentication.
41 """
42 def get_partner_by_api_key(api_key) when is_binary(api_key) do
43 Partner
44 |> where([p], p.api_key == ^api_key)
45 |> where([p], p.status == "ACTIVE")
46
:-(
|> where([p], is_nil(p.api_key_expires_at) or p.api_key_expires_at > ^DateTime.utc_now())
47
:-(
|> Repo.one()
48 end
49
50
:-(
def get_partner_by_api_key(_), do: nil
51
52 @doc """
53 Validates API key and updates last_used_at timestamp.
54 """
55 def authenticate_partner(api_key) when is_binary(api_key) do
56
:-(
case get_partner_by_api_key(api_key) do
57 %Partner{} = partner ->
58 # Update last used timestamp
59 partner
60 |> Partner.changeset(%{last_used_at: DateTime.utc_now()})
61
:-(
|> Repo.update()
62
63 {:ok, partner}
64
65
:-(
nil ->
66 {:error, :invalid_api_key}
67 end
68 end
69
70
:-(
def authenticate_partner(_), do: {:error, :invalid_api_key}
71
72 @doc """
73 Creates a partner.
74 """
75
:-(
def create_partner(attrs \\ %{}) do
76 %Partner{}
77 |> Partner.changeset(attrs)
78
:-(
|> Repo.insert()
79 end
80
81 @doc """
82 Updates a partner.
83 """
84 def update_partner(%Partner{} = partner, attrs) do
85 partner
86 |> Partner.changeset(attrs)
87
:-(
|> Repo.update()
88 end
89
90 @doc """
91 Generates a new API key for a partner.
92 """
93
:-(
def generate_api_key(%Partner{} = partner, expires_in_days \\ 365) do
94
:-(
api_key = generate_secure_key(32)
95
:-(
api_secret = generate_secure_key(64)
96
:-(
expires_at = DateTime.utc_now() |> DateTime.add(expires_in_days, :day)
97
98 partner
99 |> Partner.api_key_changeset(%{
100 api_key: api_key,
101 api_secret: api_secret,
102 api_key_expires_at: expires_at
103 })
104
:-(
|> Repo.update()
105 end
106
107 @doc """
108 Rotates the API key for a partner.
109 """
110
:-(
def rotate_api_key(%Partner{} = partner, expires_in_days \\ 365) do
111
:-(
generate_api_key(partner, expires_in_days)
112 end
113
114 @doc """
115 Suspends a partner (sets status to SUSPENDED).
116 """
117 def suspend_partner(%Partner{} = partner) do
118
:-(
update_partner(partner, %{status: "SUSPENDED"})
119 end
120
121 @doc """
122 Activates a partner (sets status to ACTIVE).
123 """
124 def activate_partner(%Partner{} = partner) do
125
:-(
update_partner(partner, %{status: "ACTIVE"})
126 end
127
128 @doc """
129 Checks if a partner is active and not expired.
130 """
131
:-(
def partner_active?(%Partner{status: "ACTIVE", api_key_expires_at: nil}), do: true
132 def partner_active?(%Partner{status: "ACTIVE", api_key_expires_at: expires_at}) do
133
:-(
DateTime.compare(expires_at, DateTime.utc_now()) == :gt
134 end
135
:-(
def partner_active?(_), do: false
136
137 @doc """
138 Checks if an IP address is whitelisted for a partner.
139 """
140
:-(
def ip_whitelisted?(%Partner{ip_whitelist: nil}, _ip), do: true
141
:-(
def ip_whitelisted?(%Partner{ip_whitelist: ""}, _ip), do: true
142 def ip_whitelisted?(%Partner{ip_whitelist: whitelist}, ip) when is_binary(whitelist) do
143
:-(
case Jason.decode(whitelist) do
144
:-(
{:ok, ip_list} when is_list(ip_list) -> ip in ip_list
145
:-(
_ -> false
146 end
147 end
148
149 @doc """
150 Updates IP whitelist for a partner.
151 """
152 def update_ip_whitelist(%Partner{} = partner, ip_list) when is_list(ip_list) do
153
:-(
case Jason.encode(ip_list) do
154 {:ok, json_ips} ->
155
:-(
update_partner(partner, %{ip_whitelist: json_ips})
156
:-(
{:error, _} ->
157 {:error, :invalid_ip_list}
158 end
159 end
160
161 @doc """
162 Gets partner rate limit (requests per minute).
163 """
164
:-(
def get_rate_limit(%Partner{rate_limit_per_minute: nil}), do: 100
165
:-(
def get_rate_limit(%Partner{rate_limit_per_minute: limit}), do: limit
166
167 # Private helper functions
168
169 defp generate_secure_key(length) do
170 length
171 |> :crypto.strong_rand_bytes()
172 |> Base.url_encode64(padding: false)
173
:-(
|> binary_part(0, length)
174 end
175
176 # ================================
177 # ADDITIONAL AUTHENTICATION FUNCTIONS
178 # ================================
179
180 @doc """
181 Checks if partner is within rate limit using Cachex.
182 """
183
:-(
def check_rate_limit(partner_id, requests_per_minute \\ 100) do
184
:-(
cache_key = "rate_limit:#{partner_id}"
185
:-(
current_minute = div(System.system_time(:second), 60)
186
:-(
window_key = "#{cache_key}:#{current_minute}"
187
188
:-(
case Cachex.get(:partner_rate_limit, window_key) do
189 {:ok, nil} ->
190 # First request in this minute window
191
:-(
Cachex.put(:partner_rate_limit, window_key, 1, ttl: :timer.minutes(1))
192 :ok
193
194 {:ok, count} when count < requests_per_minute ->
195 # Within limit, increment counter
196
:-(
Cachex.incr(:partner_rate_limit, window_key)
197 :ok
198
199
:-(
{:ok, _count} ->
200 # Rate limit exceeded
201 {:error, :rate_limit_exceeded}
202
203 {:error, _} ->
204 # Cache error, allow request but log
205 require Logger
206
:-(
Logger.warning("Rate limit cache error for partner #{partner_id}")
207 :ok
208 end
209 end
210
211 @doc """
212 Checks if IP address is whitelisted for partner.
213 """
214
:-(
def check_ip_whitelist(%Partner{ip_whitelist: nil}, _ip), do: :ok
215
:-(
def check_ip_whitelist(%Partner{ip_whitelist: ""}, _ip), do: :ok
216
217 def check_ip_whitelist(%Partner{ip_whitelist: whitelist_json}, ip) when is_binary(ip) do
218
:-(
case Jason.decode(whitelist_json) do
219 {:ok, ip_list} when is_list(ip_list) ->
220
:-(
if ip in ip_list do
221 :ok
222 else
223 {:error, :ip_not_whitelisted}
224 end
225
226
:-(
_ ->
227 # Invalid JSON, default to allow
228 :ok
229 end
230 end
231
232
:-(
def check_ip_whitelist(_, _), do: {:error, :invalid_ip}
233
234 @doc """
235 Validates API secret for critical operations (HMAC verification).
236 """
237 def validate_api_secret(%Partner{api_secret: stored_secret}, provided_secret)
238 when is_binary(stored_secret) and is_binary(provided_secret) do
239
:-(
if secure_compare(stored_secret, provided_secret) do
240 :ok
241 else
242 {:error, :invalid_secret}
243 end
244 end
245
246
:-(
def validate_api_secret(_, _), do: {:error, :invalid_secret}
247
248 # Secure string comparison to prevent timing attacks
249 defp secure_compare(a, b) when byte_size(a) == byte_size(b) do
250
:-(
:crypto.hash_equals(a, b)
251 end
252
253
:-(
defp secure_compare(_, _), do: false
254 end
Line Hits Source