cover/Elixir.WalletAuth.Commands.StartOtpChallenge.html

1 defmodule WalletAuth.Commands.StartOtpChallenge do
2 @moduledoc """
3 OTP challenge start command.
4
5 Per checklist Track C:
6 - Generates and stores an OTP challenge for the user/purpose pair.
7 - Rate-limited to prevent OTP spam.
8 - Emits `OtpChallengeStarted` event.
9
10 The generated code is returned in the result for the caller to dispatch
11 via the appropriate channel (SMS, email). `wallet_auth` does NOT dispatch
12 notifications — that is wallet_notifications' responsibility.
13 """
14
15 alias WalletAuth.Otp.OtpStore
16 alias WalletAuth.RateLimiter
17 alias WalletAuth.Events.OtpChallengeStarted
18 alias WalletObservability.AuditEvent
19
20 @type purpose :: WalletAuth.Otp.OtpStore.purpose()
21
22 @doc """
23 Starts an OTP challenge.
24
25 Returns `{:ok, code}` where `code` is the plaintext OTP to be delivered to the user.
26 Returns `{:error, :rate_limited}` if the OTP rate limit for this user is exceeded.
27 """
28 @spec execute(user_id :: String.t(), purpose(), keyword()) ::
29 {:ok, String.t()} | {:error, :rate_limited}
30
:-(
def execute(user_id, purpose, opts \\ []) do
31
:-(
correlation_id = Keyword.get(opts, :correlation_id, WalletSharedKernel.Correlation.new_correlation_id())
32
33
:-(
case RateLimiter.check_and_increment(:otp, user_id) do
34 :ok ->
35
:-(
{:ok, code} = OtpStore.start_challenge(user_id, purpose, opts)
36
37
:-(
audit = AuditEvent.build(:auth, "otp_challenge_started", "user", user_id, :success,
38 correlation_id: correlation_id,
39 metadata: %{purpose: purpose}
40 )
41
:-(
:telemetry.execute([:wallet_auth, :audit], %{}, audit)
42
43
:-(
event = OtpChallengeStarted.build(user_id, purpose,
44 correlation_id: correlation_id,
45 channel: Keyword.get(opts, :channel, :sms)
46 )
47
:-(
emit_event(event)
48
49 {:ok, code}
50
51
:-(
{:error, :rate_limited} ->
52 {:error, :rate_limited}
53 end
54 end
55
56
:-(
defp emit_event(event) do
57
:-(
pubsub = Application.get_env(:wallet_auth, :pubsub, WalletWeb.PubSub)
58
:-(
apply(Phoenix.PubSub, :broadcast, [pubsub, "wallet_auth:events", {:domain_event, event}])
59 rescue
60
:-(
_ -> :ok
61 end
62 end
Line Hits Source