standard_id 0.43.0 → 0.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +9 -2
- data/app/controllers/standard_id/web/verify_email/start_controller.rb +5 -2
- data/app/controllers/standard_id/web/verify_phone/start_controller.rb +5 -2
- data/app/mailers/standard_id/passwordless_mailer.rb +59 -3
- data/app/models/standard_id/code_challenge.rb +4 -0
- data/app/views/standard_id/passwordless_mailer/otp_email.html.erb +5 -5
- data/app/views/standard_id/passwordless_mailer/otp_email.text.erb +5 -5
- data/app/views/standard_id/passwordless_mailer/verification_email.html.erb +24 -0
- data/app/views/standard_id/passwordless_mailer/verification_email.text.erb +12 -0
- data/config/locales/en.yml +22 -0
- data/lib/standard_id/events/subscribers/passwordless_delivery_subscriber.rb +51 -17
- data/lib/standard_id/passwordless/base_strategy.rb +22 -7
- data/lib/standard_id/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8003e8ad18d3a88d9684bb3877d88bbc1d9d207a897776e6ef3a6cd01b0f1191
|
|
4
|
+
data.tar.gz: 3fb388482c096695f36d79ee02e122e1cbede9fc1d11b14c6236a4d423a82a0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2aade7f1bba3cb85827120d4ac19e55b0a2a039955097cd43af6ef75a84f1516080c4e6c0fc36a87d15731bb332603eab987c93130e69e2e2f5701a5ac929053
|
|
7
|
+
data.tar.gz: c0a8bc153ac8db1de5f5186fb014e4ed89e0b5db06ef7e295ee276f032fc59ec39a591eb6e54e440263a22d43faf8b426f6e1acf4ab09fed72870e96c4e17f85
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.43.1] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
Fixes found while shipping 0.43.0. **Take this instead of 0.43.0** — hosts on `c.passwordless.delivery = :built_in` with the WebEngine mounted (jumpdrive-web, nutripod-web) would otherwise email email-verification codes as "your sign-in code".
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Verification codes no longer use the sign-in email.** The built-in `PasswordlessDeliverySubscriber` sent `PasswordlessMailer#otp_email` ("Your sign-in code" / "Use the following code to sign in") for every realm, which 0.43.0 made reachable from WebEngine `verify_email/start` (realm `"verification"`). Only the `"authentication"` realm gets `otp_email` now. Every other realm — `verify_email`, `Otp.issue` contact verification, step-up — gets the new **`PasswordlessMailer#verification_email`** ("Your verification code" / "Use the following code to complete your verification"). Both emails state the challenge's actual expiry (the new `expires_in_minutes` param) instead of assuming `code_ttl`. There is still no built-in SMS delivery.
|
|
17
|
+
- **The WebEngine `verify_email` / `verify_phone` start 422 now gives the real reason.** The response body used to be a fixed `"invalid email"` / `"invalid phone"`; it now matches the flash: the format error, the host `username_validator`'s message, or the retry cooldown ("Please wait N seconds before requesting another code").
|
|
18
|
+
- **`PASSWORDLESS_CODE_SENT` no longer claims `delivery_status: "sent"` for codes the engine never delivered.** It was emitted as `"sent"` after every non-manual issue, including when delivery belonged to a host subscriber, or to none at all (`Otp.issue(delivery: :custom)` with no subscriber). It is still emitted in the same cases (so audit trails that record `standard_id.passwordless.*` keep their row), but `delivery_status` is now:
|
|
19
|
+
- `"sent"` — the built-in mailer enqueued the email (`deliver_later` returned a job);
|
|
20
|
+
- `"failed"` — the built-in mailer was responsible but did not enqueue it: the enqueue raised (the subscriber logs the error) or Active Job refused it (`deliver_later` returned `false`);
|
|
21
|
+
- `"delegated"` — a host `PASSWORDLESS_CODE_GENERATED` subscriber is responsible (global `:custom`, `Otp.issue(delivery: :custom)`, or SMS), so the engine cannot confirm a send.
|
|
22
|
+
|
|
23
|
+
Never emitted for `Otp.issue(delivery: :manual)`, as before. None of the five apps subscribes to `PASSWORDLESS_CODE_SENT` or reads `delivery_status`. fundbright-web, luminality-web and nutripod-web audit `standard_id.passwordless.*`, and jumpdrive-web audits all `standard_id.*`, so their stored `passwordless.code.sent` rows will now carry `"delegated"` (fundbright-web, luminality-web) or `"sent"` / `"failed"` (jumpdrive-web, nutripod-web) instead of a blanket `"sent"`.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **i18n for the built-in OTP emails** (`config/locales/en.yml`): `standard_id.passwordless_mailer.{otp_email,verification_email}.{subject,greeting,intro,expiry,ignore,footer}`. Override any key in the host's locale files. The gem ships only `en`; in any other `I18n.locale` a key the host has not translated falls back to the English copy (no "translation missing", with or without I18n fallbacks enabled). An **assigned** `c.passwordless.mailer_subject` still wins for the sign-in subject (jumpdrive-web sets one), and its unassigned default equals the i18n default, so nothing changes for a host that overrides neither.
|
|
28
|
+
- `PasswordlessDeliverySubscriber.handles?(payload)` — whether the built-in mailer delivers a given `PASSWORDLESS_CODE_GENERATED` payload.
|
|
29
|
+
- `CodeChallenge#built_in_delivered` — a transient flag (not persisted) the built-in subscriber sets once it has enqueued the email.
|
|
30
|
+
|
|
31
|
+
### Upgrade notes
|
|
32
|
+
|
|
33
|
+
Nothing is required. A host that overrides `app/views/standard_id/passwordless_mailer/otp_email.*` (luminality-web, via its React email build) keeps its sign-in template. With `:built_in` delivery, verification-realm codes now render the gem's `verification_email` template unless the host overrides that one too. luminality-web calls `PasswordlessMailer.with(...).otp_email` directly for its own verification flow; that call is unchanged.
|
|
34
|
+
|
|
10
35
|
## [0.43.0] - 2026-09-24
|
|
11
36
|
|
|
12
37
|
**Breaking minor.** Removes everything 0.42 deprecated. Every consumer cleared those warnings in its Phase 3 adoption, so for the five apps the upgrade is a version bump — see **Upgrade notes**.
|
data/README.md
CHANGED
|
@@ -554,7 +554,14 @@ end
|
|
|
554
554
|
|
|
555
555
|
Set `c.passwordless.delivery = :custom` (the default) so the engine's built-in
|
|
556
556
|
`PasswordlessMailer` stays out of the way; with `:built_in` the engine emails
|
|
557
|
-
the code itself
|
|
557
|
+
the code itself — `otp_email` ("Your sign-in code") for the `"authentication"`
|
|
558
|
+
realm and `verification_email` ("Your verification code") for every other realm
|
|
559
|
+
(WebEngine `verify_email`, `Otp.issue` contact verification / step-up). There is
|
|
560
|
+
no built-in SMS delivery. The copy is in `config/locales/en.yml` under
|
|
561
|
+
`standard_id.passwordless_mailer.{otp_email,verification_email}.*` (`subject`,
|
|
562
|
+
`greeting`, `intro`, `expiry`, `ignore`, `footer`); override any key in your own
|
|
563
|
+
locale files, or the templates in `app/views/standard_id/passwordless_mailer/`.
|
|
564
|
+
An assigned `c.passwordless.mailer_subject` still sets the sign-in subject. The subscriber runs synchronously inside the request, so
|
|
558
565
|
`I18n.locale`, `Current.*` and the like are still available.
|
|
559
566
|
|
|
560
567
|
Event payload includes:
|
|
@@ -693,7 +700,7 @@ Every StandardId event automatically carries tracing metadata (`event_id`, `time
|
|
|
693
700
|
| | `oauth.code.consumed` | `authorization_code`, `client_id`, `account` | After an authorization code is exchanged |
|
|
694
701
|
| Passwordless | `passwordless.code.requested` | `identifier`, `channel` (email/sms) | Before generating an OTP |
|
|
695
702
|
| | `passwordless.code.generated` | `code_challenge`, `identifier`, `channel`, `expires_at` | After an OTP is created |
|
|
696
|
-
| | `passwordless.code.sent` | `identifier`, `channel`, `delivery_status` | After an OTP is
|
|
703
|
+
| | `passwordless.code.sent` | `identifier`, `channel`, `realm`, `delivery_status` | After an OTP is handed to delivery (not for `Otp.issue(delivery: :manual)`). `delivery_status`: `"sent"` (built-in mailer enqueued it), `"failed"` (built-in mailer was responsible but did not enqueue), `"delegated"` (a host `PASSWORDLESS_CODE_GENERATED` subscriber delivers — the engine can't confirm it) |
|
|
697
704
|
| | `passwordless.code.verified` | `code_challenge`, `account`, `channel` | After OTP verification succeeds |
|
|
698
705
|
| | `passwordless.code.failed` | `identifier`, `channel`, `attempts` | After OTP verification fails |
|
|
699
706
|
| | `passwordless.account.created` | `account`, `channel`, `identifier` | When an account is created via passwordless flow |
|
|
@@ -32,7 +32,7 @@ module StandardId
|
|
|
32
32
|
email = params[:email].to_s.strip.downcase
|
|
33
33
|
if email.blank?
|
|
34
34
|
flash[:alert] = "Please enter your email address"
|
|
35
|
-
render plain:
|
|
35
|
+
render plain: flash[:alert], status: :unprocessable_content and return
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Issued through the passwordless strategy (validation, retry delay,
|
|
@@ -47,8 +47,11 @@ module StandardId
|
|
|
47
47
|
expires_in: 10.minutes
|
|
48
48
|
)
|
|
49
49
|
unless result.success?
|
|
50
|
+
# The body carries the same reason as the flash: a malformed
|
|
51
|
+
# target, the host's username_validator message, or the retry
|
|
52
|
+
# cooldown ("Please wait N seconds ...").
|
|
50
53
|
flash[:alert] = result.error_message
|
|
51
|
-
render plain:
|
|
54
|
+
render plain: result.error_message, status: :unprocessable_content and return
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
redirect_to standard_id_web.login_path, notice: "Verification code sent to your email", status: :see_other
|
|
@@ -32,7 +32,7 @@ module StandardId
|
|
|
32
32
|
phone = params[:phone_number].to_s.strip
|
|
33
33
|
if phone.blank? || !(phone.match?(/\A\+?[1-9]\d{1,14}\z/))
|
|
34
34
|
flash[:alert] = "Please enter a valid phone number"
|
|
35
|
-
render plain:
|
|
35
|
+
render plain: flash[:alert], status: :unprocessable_content and return
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Issued through the passwordless strategy (validation, retry delay,
|
|
@@ -47,8 +47,11 @@ module StandardId
|
|
|
47
47
|
expires_in: 10.minutes
|
|
48
48
|
)
|
|
49
49
|
unless result.success?
|
|
50
|
+
# The body carries the same reason as the flash: a malformed
|
|
51
|
+
# target, the host's username_validator message, or the retry
|
|
52
|
+
# cooldown ("Please wait N seconds ...").
|
|
50
53
|
flash[:alert] = result.error_message
|
|
51
|
-
render plain:
|
|
54
|
+
render plain: result.error_message, status: :unprocessable_content and return
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
redirect_to standard_id_web.login_path, notice: "Verification code sent via SMS", status: :see_other
|
|
@@ -1,16 +1,72 @@
|
|
|
1
1
|
module StandardId
|
|
2
|
+
# Built-in OTP emails, sent by Events::Subscribers::PasswordlessDeliverySubscriber
|
|
3
|
+
# when `c.passwordless.delivery = :built_in`.
|
|
4
|
+
#
|
|
5
|
+
# * #otp_email — the sign-in code (realm "authentication").
|
|
6
|
+
# * #verification_email — every other realm: WebEngine verify_email/start
|
|
7
|
+
# (realm "verification") and `Otp.issue(realm: ...)` for contact
|
|
8
|
+
# verification, step-up and the like. It must not tell the recipient the
|
|
9
|
+
# code is for signing in.
|
|
10
|
+
#
|
|
11
|
+
# Copy lives under `standard_id.passwordless_mailer.<action>.*` in
|
|
12
|
+
# config/locales/en.yml; override any key in the host's locale files, or
|
|
13
|
+
# override the templates in app/views/standard_id/passwordless_mailer/.
|
|
14
|
+
# `c.passwordless.mailer_subject`, when assigned, still sets the sign-in
|
|
15
|
+
# subject (it wins over the i18n key).
|
|
2
16
|
class PasswordlessMailer < ApplicationMailer
|
|
3
17
|
layout false
|
|
4
18
|
|
|
19
|
+
helper_method :otp_copy
|
|
20
|
+
|
|
21
|
+
# @param email [String]
|
|
22
|
+
# @param otp_code [String]
|
|
23
|
+
# @param expires_in_minutes [Integer, nil] defaults to passwordless.code_ttl
|
|
5
24
|
def otp_email
|
|
6
|
-
|
|
7
|
-
|
|
25
|
+
assign_otp_params
|
|
26
|
+
subject = if StandardId.config.passwordless.assigned?(:mailer_subject)
|
|
27
|
+
StandardId.config.passwordless.mailer_subject
|
|
28
|
+
else
|
|
29
|
+
otp_copy(:subject, default: StandardId.config.passwordless.mailer_subject)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
mail(to: @email, from: StandardId.config.passwordless.mailer_from, subject: subject)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param email [String]
|
|
36
|
+
# @param otp_code [String]
|
|
37
|
+
# @param realm [String, nil] the OTP realm, available to overriding templates
|
|
38
|
+
# @param expires_in_minutes [Integer, nil] defaults to passwordless.code_ttl
|
|
39
|
+
def verification_email
|
|
40
|
+
assign_otp_params
|
|
41
|
+
@realm = params[:realm]
|
|
8
42
|
|
|
9
43
|
mail(
|
|
10
44
|
to: @email,
|
|
11
45
|
from: StandardId.config.passwordless.mailer_from,
|
|
12
|
-
subject:
|
|
46
|
+
subject: otp_copy(:subject)
|
|
13
47
|
)
|
|
14
48
|
end
|
|
49
|
+
|
|
50
|
+
# Copy for the current action from
|
|
51
|
+
# standard_id.passwordless_mailer.<action>.<key>, in I18n.locale. Falls
|
|
52
|
+
# back to the gem's English copy when the host has no translation for
|
|
53
|
+
# that locale (the gem ships only `en`, and a host need not enable
|
|
54
|
+
# I18n fallbacks), so a zh-SG request never renders "translation missing".
|
|
55
|
+
#
|
|
56
|
+
# @param key [Symbol, String]
|
|
57
|
+
# @param default [String, nil] final fallback when even `en` has no key
|
|
58
|
+
def otp_copy(key, default: nil, **options)
|
|
59
|
+
scope = "standard_id.passwordless_mailer.#{action_name}"
|
|
60
|
+
english = I18n.t(key, scope: scope, locale: :en, default: default, **options)
|
|
61
|
+
I18n.t(key, scope: scope, default: english, **options)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def assign_otp_params
|
|
67
|
+
@otp_code = params[:otp_code]
|
|
68
|
+
@email = params[:email]
|
|
69
|
+
@expires_in_minutes = params[:expires_in_minutes] || (StandardId.config.passwordless.code_ttl / 60)
|
|
70
|
+
end
|
|
15
71
|
end
|
|
16
72
|
end
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
module StandardId
|
|
2
2
|
class CodeChallenge < ApplicationRecord
|
|
3
|
+
# Transient (not persisted): set by the built-in PasswordlessDeliverySubscriber
|
|
4
|
+
# once it has enqueued the email, so the passwordless strategy can report
|
|
5
|
+
# PASSWORDLESS_CODE_SENT truthfully.
|
|
6
|
+
attr_accessor :built_in_delivered
|
|
3
7
|
self.table_name = "standard_id_code_challenges"
|
|
4
8
|
|
|
5
9
|
# Well-known realms used by the engine itself. Host apps may create
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div class="container">
|
|
14
|
-
<p
|
|
15
|
-
<p
|
|
14
|
+
<p><%= otp_copy(:greeting) %></p>
|
|
15
|
+
<p><%= otp_copy(:intro) %></p>
|
|
16
16
|
<div class="code"><%= @otp_code %></div>
|
|
17
|
-
<p
|
|
18
|
-
<p
|
|
17
|
+
<p><%= otp_copy(:expiry, count: @expires_in_minutes) %></p>
|
|
18
|
+
<p><%= otp_copy(:ignore) %></p>
|
|
19
19
|
<div class="footer">
|
|
20
|
-
<p
|
|
20
|
+
<p><%= otp_copy(:footer) %></p>
|
|
21
21
|
</div>
|
|
22
22
|
</div>
|
|
23
23
|
</body>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
<%= otp_copy(:greeting) %>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<%= otp_copy(:intro) %>
|
|
4
4
|
|
|
5
5
|
<%= @otp_code %>
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<%= otp_copy(:expiry, count: @expires_in_minutes) %>
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<%= otp_copy(:ignore) %>
|
|
10
10
|
|
|
11
11
|
--
|
|
12
|
-
|
|
12
|
+
<%= otp_copy(:footer) %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<style>
|
|
6
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 0; }
|
|
7
|
+
.container { max-width: 480px; margin: 40px auto; background-color: #ffffff; border-radius: 8px; padding: 40px; }
|
|
8
|
+
.code { font-size: 32px; font-weight: bold; letter-spacing: 8px; text-align: center; padding: 20px; background-color: #f0f0f0; border-radius: 6px; margin: 24px 0; font-family: monospace; }
|
|
9
|
+
.footer { margin-top: 32px; font-size: 13px; color: #888888; text-align: center; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div class="container">
|
|
14
|
+
<p><%= otp_copy(:greeting) %></p>
|
|
15
|
+
<p><%= otp_copy(:intro) %></p>
|
|
16
|
+
<div class="code"><%= @otp_code %></div>
|
|
17
|
+
<p><%= otp_copy(:expiry, count: @expires_in_minutes) %></p>
|
|
18
|
+
<p><%= otp_copy(:ignore) %></p>
|
|
19
|
+
<div class="footer">
|
|
20
|
+
<p><%= otp_copy(:footer) %></p>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
en:
|
|
2
|
+
standard_id:
|
|
3
|
+
passwordless_mailer:
|
|
4
|
+
otp_email:
|
|
5
|
+
# Used only when c.passwordless.mailer_subject is not assigned.
|
|
6
|
+
subject: "Your sign-in code"
|
|
7
|
+
greeting: "Hi,"
|
|
8
|
+
intro: "Use the following code to sign in:"
|
|
9
|
+
expiry:
|
|
10
|
+
one: "This code will expire in %{count} minute."
|
|
11
|
+
other: "This code will expire in %{count} minutes."
|
|
12
|
+
ignore: "If you did not request this code, you can safely ignore this email."
|
|
13
|
+
footer: "This is an automated message. Please do not reply."
|
|
14
|
+
verification_email:
|
|
15
|
+
subject: "Your verification code"
|
|
16
|
+
greeting: "Hi,"
|
|
17
|
+
intro: "Use the following code to complete your verification:"
|
|
18
|
+
expiry:
|
|
19
|
+
one: "This code will expire in %{count} minute."
|
|
20
|
+
other: "This code will expire in %{count} minutes."
|
|
21
|
+
ignore: "If you did not request this code, you can safely ignore this email."
|
|
22
|
+
footer: "This is an automated message. Please do not reply."
|
|
@@ -1,31 +1,56 @@
|
|
|
1
1
|
module StandardId
|
|
2
2
|
module Events
|
|
3
3
|
module Subscribers
|
|
4
|
+
# Built-in OTP email delivery (`c.passwordless.delivery = :built_in`).
|
|
5
|
+
#
|
|
6
|
+
# Sign-in codes (realm "authentication") get PasswordlessMailer#otp_email;
|
|
7
|
+
# every other realm — WebEngine email verification, Otp.issue step-up /
|
|
8
|
+
# contact verification — gets #verification_email, so a verification code
|
|
9
|
+
# never arrives as "your sign-in code". There is no built-in SMS delivery.
|
|
10
|
+
#
|
|
11
|
+
# On a successful enqueue it marks the code challenge
|
|
12
|
+
# (CodeChallenge#built_in_delivered), which is how the strategy knows to
|
|
13
|
+
# report PASSWORDLESS_CODE_SENT as "sent" rather than "failed".
|
|
4
14
|
class PasswordlessDeliverySubscriber < Base
|
|
5
15
|
subscribe_to StandardId::Events::PASSWORDLESS_CODE_GENERATED
|
|
6
16
|
|
|
17
|
+
# Whether the built-in mailer is responsible for delivering this
|
|
18
|
+
# PASSWORDLESS_CODE_GENERATED payload.
|
|
19
|
+
#
|
|
20
|
+
# @param payload [Hash] event payload (symbol or string keys)
|
|
21
|
+
# @return [Boolean]
|
|
22
|
+
def self.handles?(payload)
|
|
23
|
+
payload = payload.with_indifferent_access
|
|
24
|
+
# Per-call manual delivery (Otp.issue(delivery: :manual)) and
|
|
25
|
+
# Otp.issue(delivery: :custom) leave delivery to the caller / host
|
|
26
|
+
# subscriber, even when the engine mailer is the global default.
|
|
27
|
+
return false if payload[:skip_sender]
|
|
28
|
+
return false if payload[:delivery]&.to_sym == :custom
|
|
29
|
+
|
|
30
|
+
StandardId.config.passwordless.delivery == :built_in && payload[:channel].to_s == "email"
|
|
31
|
+
end
|
|
32
|
+
|
|
7
33
|
def call(event)
|
|
8
|
-
|
|
9
|
-
# delivery config. Honors Otp.issue(delivery: :manual) callers who
|
|
10
|
-
# deliver the code themselves (custom widget flows, step-up
|
|
11
|
-
# challenges, etc.) and would otherwise receive a duplicate email
|
|
12
|
-
# from this subscriber when c.passwordless.delivery == :built_in.
|
|
13
|
-
return if event[:skip_sender]
|
|
14
|
-
# Otp.issue(delivery: :custom): the host's own subscriber delivers
|
|
15
|
-
# this code, even where the engine mailer is the global default.
|
|
16
|
-
return if event[:delivery]&.to_sym == :custom
|
|
17
|
-
return unless built_in_delivery?
|
|
18
|
-
return unless event[:channel] == "email"
|
|
34
|
+
return unless self.class.handles?(event.payload)
|
|
19
35
|
|
|
20
36
|
identifier = event[:identifier]
|
|
21
|
-
|
|
37
|
+
challenge = event[:code_challenge]
|
|
38
|
+
code = challenge&.code
|
|
22
39
|
|
|
23
40
|
return if identifier.blank? || code.blank?
|
|
24
41
|
|
|
25
|
-
StandardId::PasswordlessMailer.with(
|
|
42
|
+
enqueued = StandardId::PasswordlessMailer.with(
|
|
26
43
|
email: identifier,
|
|
27
|
-
otp_code: code
|
|
28
|
-
|
|
44
|
+
otp_code: code,
|
|
45
|
+
realm: event[:realm],
|
|
46
|
+
expires_in_minutes: expires_in_minutes(event[:expires_at])
|
|
47
|
+
).public_send(mailer_action(event[:realm])).deliver_later
|
|
48
|
+
|
|
49
|
+
# deliver_later returns false (without raising) when Active Job
|
|
50
|
+
# refuses the enqueue, e.g. an aborting before_enqueue callback.
|
|
51
|
+
if enqueued && challenge.respond_to?(:built_in_delivered=)
|
|
52
|
+
challenge.built_in_delivered = true
|
|
53
|
+
end
|
|
29
54
|
end
|
|
30
55
|
|
|
31
56
|
def handle_error(error, event)
|
|
@@ -37,8 +62,17 @@ module StandardId
|
|
|
37
62
|
|
|
38
63
|
private
|
|
39
64
|
|
|
40
|
-
def
|
|
41
|
-
|
|
65
|
+
def mailer_action(realm)
|
|
66
|
+
realm.blank? || realm.to_s == StandardId::Passwordless::DEFAULT_REALM ? :otp_email : :verification_email
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def expires_in_minutes(expires_at)
|
|
70
|
+
return nil if expires_at.blank?
|
|
71
|
+
|
|
72
|
+
expires_at = Time.zone.parse(expires_at) if expires_at.is_a?(String)
|
|
73
|
+
[((expires_at - Time.current) / 60.0).ceil, 1].max
|
|
74
|
+
rescue ArgumentError, TypeError, NoMethodError
|
|
75
|
+
nil
|
|
42
76
|
end
|
|
43
77
|
end
|
|
44
78
|
end
|
|
@@ -45,8 +45,8 @@ module StandardId
|
|
|
45
45
|
# can honor a per-call manual-delivery request. Without this,
|
|
46
46
|
# Otp.issue(delivery: :manual) silently double-delivers when
|
|
47
47
|
# c.passwordless.delivery == :built_in.
|
|
48
|
-
emit_code_generated(challenge, username, skip_sender: skip_sender, delivery: delivery)
|
|
49
|
-
emit_code_sent(username) unless skip_sender
|
|
48
|
+
generated_payload = emit_code_generated(challenge, username, skip_sender: skip_sender, delivery: delivery)
|
|
49
|
+
emit_code_sent(username, delivery_status: delivery_status_for(challenge, generated_payload)) unless skip_sender
|
|
50
50
|
challenge
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -190,9 +190,9 @@ module StandardId
|
|
|
190
190
|
)
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
+
# @return [Hash] the published payload (used to decide the delivery status)
|
|
193
194
|
def emit_code_generated(challenge, username, skip_sender: false, delivery: nil)
|
|
194
|
-
|
|
195
|
-
StandardId::Events::PASSWORDLESS_CODE_GENERATED,
|
|
195
|
+
payload = {
|
|
196
196
|
code_challenge: challenge,
|
|
197
197
|
identifier: username,
|
|
198
198
|
channel: connection_type,
|
|
@@ -200,16 +200,31 @@ module StandardId
|
|
|
200
200
|
expires_at: challenge.expires_at,
|
|
201
201
|
skip_sender: skip_sender,
|
|
202
202
|
delivery: delivery
|
|
203
|
-
|
|
203
|
+
}
|
|
204
|
+
StandardId::Events.publish(StandardId::Events::PASSWORDLESS_CODE_GENERATED, payload)
|
|
205
|
+
payload
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# delivery_status of PASSWORDLESS_CODE_SENT:
|
|
209
|
+
# - "sent" — the built-in mailer (delivery :built_in) enqueued the email.
|
|
210
|
+
# - "failed" — the built-in mailer was responsible but did not enqueue
|
|
211
|
+
# it (its error was logged by the subscriber).
|
|
212
|
+
# - "delegated" — delivery belongs to the host's PASSWORDLESS_CODE_GENERATED
|
|
213
|
+
# subscriber (delivery :custom, or SMS); the engine cannot
|
|
214
|
+
# know whether it sent anything.
|
|
215
|
+
def delivery_status_for(challenge, generated_payload)
|
|
216
|
+
return "delegated" unless StandardId::Events::Subscribers::PasswordlessDeliverySubscriber.handles?(generated_payload)
|
|
217
|
+
|
|
218
|
+
challenge.built_in_delivered ? "sent" : "failed"
|
|
204
219
|
end
|
|
205
220
|
|
|
206
|
-
def emit_code_sent(username)
|
|
221
|
+
def emit_code_sent(username, delivery_status:)
|
|
207
222
|
StandardId::Events.publish(
|
|
208
223
|
StandardId::Events::PASSWORDLESS_CODE_SENT,
|
|
209
224
|
identifier: username,
|
|
210
225
|
channel: connection_type,
|
|
211
226
|
realm: @realm,
|
|
212
|
-
delivery_status:
|
|
227
|
+
delivery_status: delivery_status
|
|
213
228
|
)
|
|
214
229
|
end
|
|
215
230
|
end
|
data/lib/standard_id/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.43.
|
|
4
|
+
version: 0.43.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -200,6 +200,8 @@ files:
|
|
|
200
200
|
- app/views/standard_id/password_reset_mailer/reset_email.text.erb
|
|
201
201
|
- app/views/standard_id/passwordless_mailer/otp_email.html.erb
|
|
202
202
|
- app/views/standard_id/passwordless_mailer/otp_email.text.erb
|
|
203
|
+
- app/views/standard_id/passwordless_mailer/verification_email.html.erb
|
|
204
|
+
- app/views/standard_id/passwordless_mailer/verification_email.text.erb
|
|
203
205
|
- app/views/standard_id/web/account/edit.html.erb
|
|
204
206
|
- app/views/standard_id/web/account/show.html.erb
|
|
205
207
|
- app/views/standard_id/web/auth/callback/providers/mobile_callback.html.erb
|
|
@@ -214,6 +216,7 @@ files:
|
|
|
214
216
|
- app/views/standard_id/web/signup/show.html.erb
|
|
215
217
|
- config/brakeman.ignore
|
|
216
218
|
- config/initializers/generators.rb
|
|
219
|
+
- config/locales/en.yml
|
|
217
220
|
- config/routes/api.rb
|
|
218
221
|
- config/routes/web.rb
|
|
219
222
|
- db/migrate/20250830000000_create_standard_id_client_applications.rb
|