Skip to content

✨ Added One-Time-Code flow to member sign-in#25187

Merged
kevinansfield merged 1 commit intomainfrom
release-members-OTC
Oct 22, 2025
Merged

✨ Added One-Time-Code flow to member sign-in#25187
kevinansfield merged 1 commit intomainfrom
release-members-OTC

Conversation

@kevinansfield
Copy link
Member

closes https://linear.app/ghost/issue/BER-2520/

  • Portal's sign-in flow will now include a one-time-code (OTC) in emails and display a code input form after submitting the standard email sign-in form
    • OTC usage is optional for members, magic links are still included alongside the code
    • reduces sign-in friction in scenarios such as in-app browsers
  • custom sign-in forms can opt in to the same OTC flow by adding a data-members-otc=true"
    • in this scenario, Portal will open the code input modal once a member submits their email in the custom form

@github-actions github-actions bot added the community [triage] Community features and bugs label Oct 16, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Walkthrough

This PR removes the private feature flags membersSigninOTC and membersSigninOTCAlpha from labs and FeatureService, and removes lab-flag gating for OTC (one-time code) behavior across the stack. Portal frontend, data-attributes, and site-data no longer rely on those labs flags; OTC UI and flows are driven by request/response data (includeOTC and otc_ref). Server-side MagicLink and Members router stop checking labs for OTC. Unit, integration, e2e, and portal tests were updated to reflect OTC behavior based on otc_ref/includeOTC instead of feature flags.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

core team

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the primary change by indicating that a one-time-code flow was added to the member sign-in process, which aligns with the main objective of the pull request without extraneous details or unrelated content.
Description Check ✅ Passed The description accurately outlines the introduction of a one-time-code feature in the sign-in flow, including optional usage details and how to opt in on custom forms, which directly corresponds to the implemented changes in the pull request.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch release-members-OTC

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 797548b075cc145dfe41766dbf368a4f7148e86a and b3ccf4b.

⛔ Files ignored due to path filters (3)
  • ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (16)
  • apps/admin-x-settings/src/components/settings/advanced/labs/PrivateFeatures.tsx (0 hunks)
  • apps/portal/src/actions.js (1 hunks)
  • apps/portal/src/components/pages/MagicLinkPage.js (2 hunks)
  • apps/portal/src/components/pages/MagicLinkPage.test.js (5 hunks)
  • apps/portal/src/data-attributes.js (2 hunks)
  • apps/portal/src/index.js (0 hunks)
  • apps/portal/src/tests/SigninFlow.test.js (12 hunks)
  • apps/portal/src/tests/data-attributes.test.js (3 hunks)
  • ghost/admin/app/services/feature.js (0 hunks)
  • ghost/core/core/frontend/helpers/ghost_head.js (1 hunks)
  • ghost/core/core/server/services/lib/magic-link/MagicLink.js (2 hunks)
  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js (2 hunks)
  • ghost/core/core/shared/labs.js (0 hunks)
  • ghost/core/test/e2e-api/members/send-magic-link.test.js (4 hunks)
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js (7 hunks)
  • ghost/core/test/unit/server/services/members/members-api/controllers/RouterController.test.js (5 hunks)
💤 Files with no reviewable changes (4)
  • apps/portal/src/index.js
  • ghost/core/core/shared/labs.js
  • apps/admin-x-settings/src/components/settings/advanced/labs/PrivateFeatures.tsx
  • ghost/admin/app/services/feature.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • ghost/core/core/server/services/lib/magic-link/MagicLink.js
  • apps/portal/src/tests/data-attributes.test.js
  • apps/portal/src/data-attributes.js
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js
  • ghost/core/test/unit/server/services/members/members-api/controllers/RouterController.test.js
  • ghost/core/core/frontend/helpers/ghost_head.js
🧰 Additional context used
📓 Path-based instructions (2)
ghost/core/core/server/**

📄 CodeRabbit inference engine (AGENTS.md)

Backend core logic should reside under ghost/core/core/server/

Files:

  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js
ghost/core/core/server/services/**

📄 CodeRabbit inference engine (AGENTS.md)

Backend services should be implemented under ghost/core/core/server/services/

Files:

  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js
🧠 Learnings (2)
📓 Common learnings
Learnt from: kevinansfield
PR: TryGhost/Ghost#25118
File: apps/portal/src/actions.js:160-173
Timestamp: 2025-10-09T15:31:06.587Z
Learning: When reviewing PRs that introduce feature-flagged changes (e.g., `labs?.membersSigninOTCAlpha`), avoid suggesting modifications to non-flagged code paths unless they're directly related to the PR's objectives. Keep the scope focused on the feature-flag-specific changes only.
📚 Learning: 2025-09-02T12:37:00.421Z
Learnt from: kevinansfield
PR: TryGhost/Ghost#24779
File: ghost/core/core/server/services/members/members-api/members-api.js:238-240
Timestamp: 2025-09-02T12:37:00.421Z
Learning: The otcVerification parameter in Ghost's members API functions like getTokenDataFromMagicLinkToken is optional and only relevant for OTC (one-time code) verification flows. Existing call sites that don't use OTC verification don't need to be updated as the parameter will be undefined, which is the expected behavior.

Applied to files:

  • apps/portal/src/tests/SigninFlow.test.js
🧬 Code graph analysis (5)
apps/portal/src/components/pages/MagicLinkPage.js (3)
apps/portal/src/actions.js (1)
  • otcRef (125-125)
apps/portal/src/data-attributes.js (1)
  • otcRef (103-103)
ghost/core/test/e2e-api/members/send-magic-link.test.js (2)
  • otcRef (918-918)
  • otcRef (1016-1016)
apps/portal/src/actions.js (1)
ghost/core/test/e2e-api/members/send-magic-link.test.js (7)
  • response (802-803)
  • response (809-810)
  • response (850-851)
  • response (865-866)
  • response (883-884)
  • response (903-904)
  • response (911-912)
apps/portal/src/tests/SigninFlow.test.js (1)
apps/portal/src/App.js (1)
  • App (45-1045)
apps/portal/src/components/pages/MagicLinkPage.test.js (2)
apps/portal/src/tests/SigninFlow.test.js (2)
  • utils (29-31)
  • utils (85-87)
apps/portal/src/utils/test-utils.js (2)
  • utils (34-34)
  • utils (45-45)
ghost/core/test/e2e-api/members/send-magic-link.test.js (6)
apps/portal/src/actions.js (4)
  • email (124-124)
  • response (90-90)
  • response (149-149)
  • otcRef (125-125)
apps/portal/src/data-attributes.js (4)
  • email (30-30)
  • otcRef (103-103)
  • i (37-37)
  • i (42-42)
ghost/core/core/server/services/members/members-api/controllers/RouterController.js (6)
  • email (133-133)
  • email (535-535)
  • response (575-575)
  • require (4-4)
  • require (6-6)
  • redirectUrl (734-734)
ghost/core/core/server/services/members/SingleUseTokenProvider.js (2)
  • require (2-2)
  • require (5-5)
ghost/core/core/server/web/shared/middleware/api/spam-prevention.js (1)
  • otcVerification (290-319)
ghost/core/test/utils/e2e-framework.js (1)
  • resetRateLimits (174-178)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Setup
  • GitHub Check: Build & Push
  • GitHub Check: Setup
🔇 Additional comments (10)
ghost/core/core/server/services/members/members-api/controllers/RouterController.js (2)

668-671: LGTM! OTC response now driven by data rather than feature flags.

The removal of feature flag gating is correct. The OTC reference is returned when present, aligning with the PR objective to drive OTC flows via request/response data (otcRef) instead of labs flags.


793-795: LGTM! Explicit type checking for includeOTC parameter.

The explicit checks for both boolean true and string 'true' provide clear handling of different input types. The removal of feature flag gating correctly shifts OTC control to request parameters.

apps/portal/src/components/pages/MagicLinkPage.test.js (1)

9-9: LGTM! Labs-based gating correctly removed from tests.

The test updates properly reflect that OTC rendering is now driven solely by otcRef presence rather than feature flags. The edge case coverage at lines 314-323 appropriately verifies that OTC form is not rendered when otcRef is null, regardless of labs configuration.

Also applies to: 36-36, 80-98, 314-323

apps/portal/src/components/pages/MagicLinkPage.js (1)

225-230: LGTM! Component correctly simplified to use otcRef-only gating.

The removal of labs checks and simplification to otcRef-only gating at lines 225-230 and 284-285 is correct and aligns with the PR's objective to make OTC behavior response-driven.

Also applies to: 284-285

apps/portal/src/actions.js (2)

92-102: LGTM! Simplified OTC flow control logic.

Checking only response?.otc_ref (line 92) to determine OTC flow is correct and cleaner than the previous dual-condition check. The server response now fully controls whether OTC is available.


88-88: IncludeOTC flag is handled correctly server-side
RouterController and MagicLink service respect the flag—only generating an OTC when includeOTC=true; all related tests pass.

apps/portal/src/tests/SigninFlow.test.js (4)

123-131: LGTM! Excellent test helper for DRY principle.

The expectOTCEnabledSendMagicLinkAPICall helper consolidates repeated assertions and improves test maintainability by centralizing the verification of OTC-enabled API calls.


153-156: LGTM! Test correctly reflects OTC-enabled default flow.

The mock update to return otc_ref (lines 153-156) and usage of the new helper (line 175) correctly reflects that OTC is now part of the default signin flow rather than a feature-flagged variant.

Also applies to: 175-176


416-416: LGTM! Labs flag removal confirmed in OTC integration tests.

Passing empty labs={{}} object confirms that OTC behavior is no longer gated by feature flags and relies solely on server response (otcRef).


457-458: LGTM! Consistent use of OTC helper in integration tests.

All OTC integration tests consistently use expectOTCEnabledSendMagicLinkAPICall to verify that includeOTC: true is always sent, and verify call counts appropriately.

Also applies to: 484-485, 501-501


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Oct 16, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.43%. Comparing base (f4eb79e) to head (b3ccf4b).
⚠️ Report is 37 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25187      +/-   ##
==========================================
- Coverage   71.44%   71.43%   -0.02%     
==========================================
  Files        1529     1529              
  Lines      116172   116170       -2     
  Branches    13983    13978       -5     
==========================================
- Hits        82995    82981      -14     
- Misses      32168    32198      +30     
+ Partials     1009      991      -18     
Flag Coverage Δ
admin-tests 47.91% <ø> (ø)
e2e-tests 71.43% <100.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
apps/portal/src/components/pages/MagicLinkPage.test.js (1)

314-323: Rename stale test title mentioning lab flag

Title says “even with lab flag” but labs are empty. Rename for clarity.

-        test('does not render form without otcRef even with lab flag', () => {
+        test('does not render form without otcRef', () => {
apps/portal/src/tests/SigninFlow.test.js (1)

143-147: Consistent OTC signaling and assertions

Good: signin payloads include includeOTC: true; mocks return otc_ref; labs default to {}. Matches OTC-by-request behavior. Based on learnings.

You already have expectOTCEnabledApiCall below; consider reusing it in earlier tests to reduce duplication.

Also applies to: 194-199, 223-229, 268-273, 297-302, 326-331, 436-437

ghost/core/test/unit/server/services/lib/magic-link/index.test.js (1)

104-121: Harden assertions and lock intended scope (signin-only)

  • Avoid brittle call-count: asserting “calledTwice” ties tests to internal implementation.
  • Add a test ensuring includeOTC is ignored for signup (intent: OTC is for sign-in).

Apply this to relax the call-count dependency:

-            assert(mockSingleUseTokenProvider.getRefByToken.calledTwice);
+            assert(mockSingleUseTokenProvider.getRefByToken.callCount >= 1);

Add a test to lock signup behavior:

it('does not include OTC for signup even when includeOTC=true', async function () {
    const options = buildOptions();
    const service = new MagicLink(options);
    const args = {
        email: 'test@example.com',
        tokenData: {id: '420'},
        includeOTC: true,
        type: 'signup'
    };
    const result = await service.sendMagicLink(args);
    assert.equal(result.otcRef, null);
    assert(options.getText.firstCall.calledWithExactly('FAKEURL', 'signup', 'test@example.com', null));
    assert(options.getHTML.firstCall.calledWithExactly('FAKEURL', 'signup', 'test@example.com', null));
    assert(options.getSubject.firstCall.calledWithExactly('signup', null));
});

Please confirm the intended constraint is “OTC applies to signin only.”

Also applies to: 125-141, 145-162, 163-187, 188-211, 213-235, 239-260

ghost/core/test/e2e-api/members/send-magic-link.test.js (1)

792-797: Tighten helper semantics and OTC extraction; add signup guard

  • Ensure we can explicitly test includeOTC: false (boolean) vs missing param:

    • Update helper to send the field when false is passed.
  • Reduce chance of matching unrelated numbers when extracting the code:

    • Prefer a label-anchored regex if available in templates.
  • Helper change:

 function sendMagicLinkRequest(email, emailType = 'signin', otc = false) {
     const body = {email, emailType};
-    if (otc) {
+    if (otc !== undefined) {
         body.includeOTC = otc;
     }
     return membersAgent.post('/api/send-magic-link').body(body);
 }
  • OTC extraction (if email content includes a “Code:” label):
-    const otc = mail.text.match(/\d{6}/)[0];
+    const match = mail.text.match(/code:\s*(\d{6})/i);
+    const otc = match && match[1];
  • Add a guard test to ensure signup never includes OTC even if requested:
it('Does not include OTC for signup even when includeOTC=true', async function () {
    const response = await sendMagicLinkRequest('member1@test.com', 'signup', true).expectStatus(201);
    assert(!response.body.otc_ref);
    const mail = mockManager.assert.sentEmail({to: 'member1@test.com'});
    assertNoOTCInEmailContent(mail);
});

Also applies to: 799-804, 806-811, 813-821, 845-858, 860-873, 907-929

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1570e and 9e1554d53419c67ea9c3fec1a4e65890cb9097ce.

⛔ Files ignored due to path filters (3)
  • ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (16)
  • apps/admin-x-settings/src/components/settings/advanced/labs/PrivateFeatures.tsx (0 hunks)
  • apps/portal/src/actions.js (1 hunks)
  • apps/portal/src/components/pages/MagicLinkPage.js (2 hunks)
  • apps/portal/src/components/pages/MagicLinkPage.test.js (5 hunks)
  • apps/portal/src/data-attributes.js (1 hunks)
  • apps/portal/src/index.js (0 hunks)
  • apps/portal/src/tests/SigninFlow.test.js (7 hunks)
  • apps/portal/src/tests/data-attributes.test.js (3 hunks)
  • ghost/admin/app/services/feature.js (0 hunks)
  • ghost/core/core/frontend/helpers/ghost_head.js (1 hunks)
  • ghost/core/core/server/services/lib/magic-link/MagicLink.js (2 hunks)
  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js (2 hunks)
  • ghost/core/core/shared/labs.js (0 hunks)
  • ghost/core/test/e2e-api/members/send-magic-link.test.js (3 hunks)
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js (7 hunks)
  • ghost/core/test/unit/server/services/members/members-api/controllers/RouterController.test.js (5 hunks)
💤 Files with no reviewable changes (4)
  • apps/portal/src/index.js
  • ghost/admin/app/services/feature.js
  • apps/admin-x-settings/src/components/settings/advanced/labs/PrivateFeatures.tsx
  • ghost/core/core/shared/labs.js
🧰 Additional context used
📓 Path-based instructions (3)
ghost/core/core/frontend/**

📄 CodeRabbit inference engine (AGENTS.md)

Frontend and theme rendering for core should be under ghost/core/core/frontend/

Files:

  • ghost/core/core/frontend/helpers/ghost_head.js
ghost/core/core/server/**

📄 CodeRabbit inference engine (AGENTS.md)

Backend core logic should reside under ghost/core/core/server/

Files:

  • ghost/core/core/server/services/lib/magic-link/MagicLink.js
  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js
ghost/core/core/server/services/**

📄 CodeRabbit inference engine (AGENTS.md)

Backend services should be implemented under ghost/core/core/server/services/

Files:

  • ghost/core/core/server/services/lib/magic-link/MagicLink.js
  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js
🧠 Learnings (2)
📓 Common learnings
Learnt from: kevinansfield
PR: TryGhost/Ghost#25118
File: apps/portal/src/actions.js:160-173
Timestamp: 2025-10-09T15:31:06.587Z
Learning: When reviewing PRs that introduce feature-flagged changes (e.g., `labs?.membersSigninOTCAlpha`), avoid suggesting modifications to non-flagged code paths unless they're directly related to the PR's objectives. Keep the scope focused on the feature-flag-specific changes only.
📚 Learning: 2025-09-02T12:37:00.421Z
Learnt from: kevinansfield
PR: TryGhost/Ghost#24779
File: ghost/core/core/server/services/members/members-api/members-api.js:238-240
Timestamp: 2025-09-02T12:37:00.421Z
Learning: The otcVerification parameter in Ghost's members API functions like getTokenDataFromMagicLinkToken is optional and only relevant for OTC (one-time code) verification flows. Existing call sites that don't use OTC verification don't need to be updated as the parameter will be undefined, which is the expected behavior.

Applied to files:

  • ghost/core/core/server/services/lib/magic-link/MagicLink.js
  • apps/portal/src/tests/SigninFlow.test.js
🧬 Code graph analysis (6)
ghost/core/core/frontend/helpers/ghost_head.js (2)
ghost/core/core/shared/labs.js (1)
  • settingsCache (13-13)
ghost/core/core/frontend/services/proxy.js (1)
  • settingsCache (2-2)
apps/portal/src/actions.js (1)
ghost/core/test/e2e-api/members/send-magic-link.test.js (7)
  • response (799-800)
  • response (806-807)
  • response (847-848)
  • response (862-863)
  • response (880-881)
  • response (900-901)
  • response (908-909)
apps/portal/src/components/pages/MagicLinkPage.js (3)
apps/portal/src/actions.js (1)
  • otcRef (125-125)
apps/portal/src/data-attributes.js (1)
  • otcRef (103-103)
ghost/core/test/e2e-api/members/send-magic-link.test.js (2)
  • otcRef (915-915)
  • otcRef (1013-1013)
ghost/core/test/e2e-api/members/send-magic-link.test.js (3)
ghost/core/core/server/services/members/SingleUseTokenProvider.js (2)
  • require (2-2)
  • require (5-5)
ghost/core/core/server/web/shared/middleware/api/spam-prevention.js (1)
  • otcVerification (290-319)
ghost/core/test/utils/e2e-framework.js (1)
  • resetRateLimits (174-178)
apps/portal/src/tests/data-attributes.test.js (2)
apps/portal/src/index.js (1)
  • labs (27-27)
ghost/core/core/shared/labs.js (1)
  • labs (59-59)
apps/portal/src/tests/SigninFlow.test.js (1)
apps/portal/src/App.js (1)
  • App (45-1045)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Admin tests - Chrome
  • GitHub Check: Legacy tests (Node 22.13.1, sqlite3)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Legacy tests (Node 22.13.1, mysql8)
  • GitHub Check: Acceptance tests (Node 22.13.1, mysql8)
  • GitHub Check: Acceptance tests (Node 22.13.1, sqlite3)
  • GitHub Check: Unit tests (Node 22.13.1)
  • GitHub Check: Build & Push
🔇 Additional comments (14)
ghost/core/core/frontend/helpers/ghost_head.js (1)

66-66: LGTM! OTC feature flag attributes removed correctly.

The removal of OTC-related data attributes aligns with the PR objective to remove feature flag gating. The locale attribute correctly remains in place.

Note: The AI summary incorrectly states that the locale attribute was removed, but the code shows it's still present.

ghost/core/core/server/services/members/members-api/controllers/RouterController.js (2)

668-671: LGTM! OTC flow now driven by response data.

The check for signIn.otcRef correctly determines whether to return OTC reference in the response, removing dependency on labs feature flags.


789-806: LGTM! Request-driven OTC handling.

The OTC inclusion is now determined solely by the request body parameter, removing labs-based gating. The defensive check for both boolean true and string 'true' handles potential type variations from different client implementations.

ghost/core/core/server/services/lib/magic-link/MagicLink.js (2)

88-95: LGTM! OTC generation driven by request options.

The conditional OTC generation now relies solely on options.includeOTC, removing labs-based feature flag dependency. Error handling with Sentry capture remains intact.


109-116: LGTM! otcRef returned only when OTC successfully generated.

The logic correctly returns otcRef only when an OTC has been successfully generated, preventing clients from showing OTC input fields when the email doesn't contain a code.

ghost/core/test/unit/server/services/members/members-api/controllers/RouterController.test.js (1)

860-929: LGTM! Tests updated to reflect otcRef-driven OTC flow.

The test suite correctly validates the new behavior where OTC response handling is driven by the presence of otcRef in the response, independent of labs feature flags. The test fixture at line 883 appropriately sets labsService.isSet to return false, confirming the removal of labs-based gating.

Test coverage includes:

  • OTC reference returned when present
  • Standard response when otcRef is null
  • Standard response when otcRef is undefined
apps/portal/src/data-attributes.js (1)

50-50: LGTM! OTC eligibility simplified.

The OTC flag evaluation now depends solely on the form's data attributes (data-members-otc="true") and email type, removing labs feature flag dependency. This aligns with the PR objective allowing custom forms to opt into the OTC flow.

apps/portal/src/tests/data-attributes.test.js (2)

183-229: LGTM! Test updated to reflect attribute-driven OTC flow.

The test correctly validates that OTC magic link requests and Portal opening are triggered when data-members-otc="true" is set on the form, independent of labs flags. The empty labs object at line 196 confirms the removal of feature flag dependency.


231-283: LGTM! Exception handling test updated.

The test validates that OTC action exceptions are properly captured and logged, with the empty labs object at line 244 confirming the removal of feature flag dependency.

apps/portal/src/components/pages/MagicLinkPage.js (2)

224-281: LGTM! OTC form rendering driven by otcRef presence.

The renderOTCForm method now relies solely on the presence of otcRef to determine whether to display the OTC input form, removing labs feature flag dependency. The early return at line 228-230 ensures the form is only rendered when an OTC reference exists.


283-294: LGTM! Simplified OTC form visibility logic.

The render logic cleanly determines OTC form visibility based solely on otcRef presence, removing all labs-based conditional rendering. This provides a clear, single source of truth for whether to show the OTC input interface.

apps/portal/src/actions.js (1)

81-111: Backend OTC implementation is efficient with proper safeguards in place—no action required.

The unconditional includeOTC: true change is safe. Backend analysis confirms:

  • OTC derivation is lightweight: deriveOTC() uses deterministic HMAC hashing, not expensive generation
  • Rate limiting enforced: Spam prevention middleware applies multiple limits (otc_verification.freeRetries, otc_verification_enumeration.freeRetries) to prevent abuse and brute force
  • Graceful error handling: System continues without OTC if derivation fails (not a critical path)
  • Security safeguards: Constant-time comparison (crypto.timingSafeEqual) prevents timing attacks

The change aligns with PR objectives and backend architecture supports unconditional OTC inclusion without performance concerns.

apps/portal/src/components/pages/MagicLinkPage.test.js (1)

7-14: OTC rendering keyed to otcRef-only looks good

Good cleanup: labs removed; OTC form visibility correctly depends solely on otcRef.

Also applies to: 33-41, 80-86, 87-99

ghost/core/test/e2e-api/members/send-magic-link.test.js (1)

336-340: OTC email snapshot looks good

Scrubbing six-digit sequences keeps snapshots stable while validating OTC presence.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (9)
apps/portal/src/tests/data-attributes.test.js (2)

183-229: OTC via data-members-otc works as intended; labs no longer needed here

Test correctly asserts includeOTC and startSigninOTCFromCustomForm with trimmed email. Consider dropping the unused labs parameter when calling formSubmitHandler to reduce noise.


244-283: OTC error capture test is solid; trim + redundant labs

Assertions on captureException/console.error are good and resilient. labs is no longer used by formSubmitHandler; you can omit it from the call to make intent clearer.

apps/portal/src/components/pages/MagicLinkPage.test.js (2)

9-13: Default labs stub is fine, but redundant

labs isn’t used for OTC anymore; you can drop it from setupTest defaults to declutter.


36-40: setupOTCTest: labs override can be removed

Given gating is only on otcRef, labs: {} is unnecessary here.

apps/portal/src/data-attributes.js (1)

19-20: Signature cleanup is correct; remove stale labs plumbing

formSubmitHandler no longer uses labs. handleDataAttributes still accepts/forwards labs; remove it to avoid dead params.

Apply this diff in handleDataAttributes to drop labs:

-export function handleDataAttributes({siteUrl, site = {}, member, labs = {}, doAction, captureException} = {}) {
+export function handleDataAttributes({siteUrl, site = {}, member, doAction, captureException} = {}) {
@@
-        function submitHandler(event) {
-            formSubmitHandler({event, errorEl, form, siteUrl, submitHandler, labs, doAction, captureException});
-        }
+        function submitHandler(event) {
+            formSubmitHandler({event, errorEl, form, siteUrl, submitHandler, doAction, captureException});
+        }
apps/portal/src/actions.js (1)

92-101: Show OTC UI only when otc_ref present

Correct gating. Optional: trim email in payload to de-whitespace user input (pageData already trims).

Apply:

-        const payload = {
-            ...data,
+        const payload = {
+            ...data,
+            email: (data?.email || '').trim(),
             emailType: 'signin',
             integrityToken,
             includeOTC: true
         };
apps/portal/src/tests/SigninFlow.test.js (1)

436-437: labs prop in OTC Integration Flow is unnecessary

Since OTC is not labs-gated, you can omit labs={{}} when rendering App to simplify tests.

ghost/core/test/e2e-api/members/send-magic-link.test.js (2)

401-416: Avoid brittle exact body keys in blocked-domain signin tests

Future API additions could break Object.keys(body).should.eql(['otc_ref']). Prefer asserting presence/type only.

Apply:

-                    .expect(({body}) => {
-                        Object.keys(body).should.eql(['otc_ref']);
-                        body.otc_ref.should.be.a.String().and.match(/^[a-f0-9-]{36}$/);
-                    });
+                    .expect(({body}) => {
+                        should.exist(body.otc_ref);
+                        body.otc_ref.should.be.a.String().and.match(/^[a-f0-9-]{36}$/);
+                    });

And similarly for the second test:

-                    .expect(({body}) => {
-                        should.exist(body.otc_ref);
-                        body.otc_ref.should.be.a.String().and.match(/^[a-f0-9-]{36}$/);
-                    });
+                    .expect(({body}) => {
+                        should.exist(body.otc_ref);
+                        body.otc_ref.should.be.a.String().and.match(/^[a-f0-9-]{36}$/);
+                    });

Also applies to: 419-436


812-820: Add explicit status/body assertions to reduce flakiness

A couple of tests await the request but don't assert status/body. Add expectations to fail fast on regressions. Also, optionally tighten the "no OTC" regex to avoid false positives on the word "code".

Apply:

-            await sendMagicLinkRequest('member1@test.com', 'signin', true);
+            await sendMagicLinkRequest('member1@test.com', 'signin', true)
+                .expectStatus(201);
-                await sendMagicLinkRequest('member1@test.com', emailType, false);
+                await sendMagicLinkRequest('member1@test.com', emailType, false)
+                    .expectEmptyBody()
+                    .expectStatus(201);

Optional (outside this hunk): refine the no-OTC detector to use word boundaries.

-            const otcRegex = /\d{6}|\scode\s|\sotc\s/i;
+            const otcRegex = /\d{6}|\bcode\b|\botc\b/i;

Also applies to: 822-831

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e1554d53419c67ea9c3fec1a4e65890cb9097ce and 33ed1afbb6763326a0ef41ed12929773389aeb01.

⛔ Files ignored due to path filters (3)
  • ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (16)
  • apps/admin-x-settings/src/components/settings/advanced/labs/PrivateFeatures.tsx (0 hunks)
  • apps/portal/src/actions.js (1 hunks)
  • apps/portal/src/components/pages/MagicLinkPage.js (2 hunks)
  • apps/portal/src/components/pages/MagicLinkPage.test.js (5 hunks)
  • apps/portal/src/data-attributes.js (2 hunks)
  • apps/portal/src/index.js (0 hunks)
  • apps/portal/src/tests/SigninFlow.test.js (7 hunks)
  • apps/portal/src/tests/data-attributes.test.js (3 hunks)
  • ghost/admin/app/services/feature.js (0 hunks)
  • ghost/core/core/frontend/helpers/ghost_head.js (1 hunks)
  • ghost/core/core/server/services/lib/magic-link/MagicLink.js (2 hunks)
  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js (2 hunks)
  • ghost/core/core/shared/labs.js (0 hunks)
  • ghost/core/test/e2e-api/members/send-magic-link.test.js (3 hunks)
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js (7 hunks)
  • ghost/core/test/unit/server/services/members/members-api/controllers/RouterController.test.js (5 hunks)
💤 Files with no reviewable changes (4)
  • ghost/admin/app/services/feature.js
  • ghost/core/core/shared/labs.js
  • apps/portal/src/index.js
  • apps/admin-x-settings/src/components/settings/advanced/labs/PrivateFeatures.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/portal/src/components/pages/MagicLinkPage.js
  • ghost/core/core/server/services/lib/magic-link/MagicLink.js
  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js
  • ghost/core/core/frontend/helpers/ghost_head.js
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: kevinansfield
PR: TryGhost/Ghost#25118
File: apps/portal/src/actions.js:160-173
Timestamp: 2025-10-09T15:31:06.587Z
Learning: When reviewing PRs that introduce feature-flagged changes (e.g., `labs?.membersSigninOTCAlpha`), avoid suggesting modifications to non-flagged code paths unless they're directly related to the PR's objectives. Keep the scope focused on the feature-flag-specific changes only.
📚 Learning: 2025-09-02T12:37:00.421Z
Learnt from: kevinansfield
PR: TryGhost/Ghost#24779
File: ghost/core/core/server/services/members/members-api/members-api.js:238-240
Timestamp: 2025-09-02T12:37:00.421Z
Learning: The otcVerification parameter in Ghost's members API functions like getTokenDataFromMagicLinkToken is optional and only relevant for OTC (one-time code) verification flows. Existing call sites that don't use OTC verification don't need to be updated as the parameter will be undefined, which is the expected behavior.

Applied to files:

  • apps/portal/src/tests/SigninFlow.test.js
🧬 Code graph analysis (3)
apps/portal/src/actions.js (1)
ghost/core/test/e2e-api/members/send-magic-link.test.js (7)
  • response (799-800)
  • response (806-807)
  • response (847-848)
  • response (862-863)
  • response (880-881)
  • response (900-901)
  • response (908-909)
apps/portal/src/tests/SigninFlow.test.js (1)
apps/portal/src/App.js (1)
  • App (45-1045)
apps/portal/src/tests/data-attributes.test.js (2)
apps/portal/src/index.js (1)
  • labs (27-27)
ghost/core/core/shared/labs.js (1)
  • labs (59-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: E2E Tests (Shard 2/2)
  • GitHub Check: E2E Tests (Shard 1/2)
  • GitHub Check: Inspect Docker Image
  • GitHub Check: Legacy tests (Node 22.13.1, sqlite3)
  • GitHub Check: Legacy tests (Node 22.13.1, mysql8)
  • GitHub Check: Lint
  • GitHub Check: Admin tests - Chrome
  • GitHub Check: Acceptance tests (Node 22.13.1, mysql8)
  • GitHub Check: Acceptance tests (Node 22.13.1, sqlite3)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Unit tests (Node 22.13.1)
🔇 Additional comments (24)
apps/portal/src/components/pages/MagicLinkPage.test.js (3)

80-86: Gating OTC form solely on otcRef is correct

Cleanly reflects the new flow; looks good.


89-97: Negative scenario reads well

Ensures form is absent without otcRef; good coverage.


316-323: No otcRef → no form, regardless of labs

Accurately tests the updated behavior; LGTM.

ghost/core/test/unit/server/services/lib/magic-link/index.test.js (7)

104-120: No includeOTC → no otcRef

Correctly asserts otcRef omission and no ref lookup when includeOTC isn’t set.


125-141: Graceful handling without getRefByToken

Good fallback: continues email send, otcRef null.


145-161: Null ref path covered

Skips deriveOTC and keeps otcRef null; good defensive test.


163-187: includeOTC=true path verifies ref + OTC propagation into emails

Asserts tokenProvider interactions and OTC inclusion in subject/text/html; solid coverage.


188-210: includeOTC=false path verified

Ensures no OTC calls and null passed to email content functions.


213-234: Email content functions assertions (OTC) are precise

Validates argument wiring; looks good.


239-260: Missing deriveOTC handled cleanly

OTC omitted when deriveOTC is unavailable; expected behavior.

ghost/core/test/unit/server/services/members/members-api/controllers/RouterController.test.js (3)

883-884: labsService isSet(false) is fine; behavior no longer gated by labs

Keeps tests honest about flag removal; OTC behavior is response-driven.


904-911: otc_ref returned only when present

Checks Content-Type and body; aligns with RouterController behavior.


913-929: No otcRef → standard 201 Created

Negative cases (null/undefined) handled; correct response shape asserted.

apps/portal/src/actions.js (1)

88-89: Always request OTC for Portal sign-in

This aligns with the new flow; server will include magic link + code when available.

apps/portal/src/tests/SigninFlow.test.js (2)

143-147: Explicitly mocking otc_ref is appropriate

Ensures OTC path is exercised; good setup for the default signin test.


197-199: Expecting includeOTC: true is correct across signin scenarios

Assertions now reflect Portal’s unconditional OTC request.

Also applies to: 226-228, 272-273, 300-302, 330-331

apps/portal/src/data-attributes.js (1)

50-50: OTC condition is good; trim email to avoid whitespace issues

wantsOTC logic matches the new flow. Trim the email before sending to the API to avoid leading/trailing spaces from custom forms.

-    let email = emailInput?.value;
+    let email = emailInput?.value?.trim();

The search for leftover labs-based OTC flags (rg -n 'membersSigninOTC' and rg -n 'dataset\.\s*membersOtc') returned no matches; please manually confirm no flags remain.

ghost/core/test/e2e-api/members/send-magic-link.test.js (7)

336-341: OTC signin snapshot looks good

Snapshot adds OTC scenario with scrubbing — solid coverage.


792-811: Response-shape assertions for includeOTC vs default are correct

Empty body by default; otc_ref present only when requested — LGTM.


845-873: includeOTC param coercion tests (true/false, string variants) look good

Covers both boolean and string cases; assertions on response and email content are solid.


875-897: OTC generation failure path is well handled

Stubbing deriveOTC to throw and asserting success without OTC is the right behavior. Cleanup via finally is correct.


899-905: Non-existent member + includeOTC returns 400 without otc_ref

Behavior and assertions make sense. Good negative coverage.


907-944: /verify-otc happy-path and redirect precedence tests are solid

Helper is clean; assertions on token and otc_verification presence and r param precedence look correct.

Also applies to: 946-958


960-977: OTC verification rate-limiting tests are comprehensive

Covers IP-based enumeration and per-otcRef counters, plus independence between refs. Config override/reset hygiene looks correct.

Also applies to: 979-1009, 1011-1042, 1044-1079

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/portal/src/data-attributes.js (1)

209-223: Remove unused labs parameter.

The labs parameter is passed to formSubmitHandler at line 220, but after the changes at line 19, formSubmitHandler no longer uses it. The parameter can be safely removed from this function call.

Apply this diff to remove the unused parameter:

         let errorEl = form.querySelector('[data-members-error]');
         function submitHandler(event) {
-            formSubmitHandler({event, errorEl, form, siteUrl, submitHandler, labs, doAction, captureException});
+            formSubmitHandler({event, errorEl, form, siteUrl, submitHandler, doAction, captureException});
         }
         form.addEventListener('submit', submitHandler);
ghost/core/core/server/services/lib/magic-link/MagicLink.js (1)

47-59: Consider removing unused labsService property.

After the changes at lines 88 and 109, the labsService property is no longer used in this class. Consider removing it from the constructor options and instance properties to keep the code clean.

Apply this diff to remove the unused property:

     constructor(options) {
         if (!options || !options.transporter || !options.tokenProvider || !options.getSigninURL) {
             throw new IncorrectUsageError({message: 'Missing options. Expects {transporter, tokenProvider, getSigninURL}'});
         }
         this.transporter = options.transporter;
         this.tokenProvider = options.tokenProvider;
         this.getSigninURL = options.getSigninURL;
         this.getText = options.getText || defaultGetText;
         this.getHTML = options.getHTML || defaultGetHTML;
         this.getSubject = options.getSubject || defaultGetSubject;
         this.sentry = options.sentry || undefined;
-        this.labsService = options.labsService || undefined;
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33ed1afbb6763326a0ef41ed12929773389aeb01 and 797548b075cc145dfe41766dbf368a4f7148e86a.

⛔ Files ignored due to path filters (3)
  • ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap is excluded by !**/*.snap
  • ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (16)
  • apps/admin-x-settings/src/components/settings/advanced/labs/PrivateFeatures.tsx (0 hunks)
  • apps/portal/src/actions.js (1 hunks)
  • apps/portal/src/components/pages/MagicLinkPage.js (2 hunks)
  • apps/portal/src/components/pages/MagicLinkPage.test.js (5 hunks)
  • apps/portal/src/data-attributes.js (2 hunks)
  • apps/portal/src/index.js (0 hunks)
  • apps/portal/src/tests/SigninFlow.test.js (12 hunks)
  • apps/portal/src/tests/data-attributes.test.js (3 hunks)
  • ghost/admin/app/services/feature.js (0 hunks)
  • ghost/core/core/frontend/helpers/ghost_head.js (1 hunks)
  • ghost/core/core/server/services/lib/magic-link/MagicLink.js (2 hunks)
  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js (2 hunks)
  • ghost/core/core/shared/labs.js (0 hunks)
  • ghost/core/test/e2e-api/members/send-magic-link.test.js (3 hunks)
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js (7 hunks)
  • ghost/core/test/unit/server/services/members/members-api/controllers/RouterController.test.js (5 hunks)
💤 Files with no reviewable changes (4)
  • ghost/core/core/shared/labs.js
  • apps/portal/src/index.js
  • ghost/admin/app/services/feature.js
  • apps/admin-x-settings/src/components/settings/advanced/labs/PrivateFeatures.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js
  • apps/portal/src/components/pages/MagicLinkPage.js
  • apps/portal/src/tests/data-attributes.test.js
🧰 Additional context used
📓 Path-based instructions (3)
ghost/core/core/frontend/**

📄 CodeRabbit inference engine (AGENTS.md)

Frontend and theme rendering for core should be under ghost/core/core/frontend/

Files:

  • ghost/core/core/frontend/helpers/ghost_head.js
ghost/core/core/server/**

📄 CodeRabbit inference engine (AGENTS.md)

Backend core logic should reside under ghost/core/core/server/

Files:

  • ghost/core/core/server/services/lib/magic-link/MagicLink.js
ghost/core/core/server/services/**

📄 CodeRabbit inference engine (AGENTS.md)

Backend services should be implemented under ghost/core/core/server/services/

Files:

  • ghost/core/core/server/services/lib/magic-link/MagicLink.js
🧠 Learnings (2)
📓 Common learnings
Learnt from: kevinansfield
PR: TryGhost/Ghost#25118
File: apps/portal/src/actions.js:160-173
Timestamp: 2025-10-09T15:31:06.587Z
Learning: When reviewing PRs that introduce feature-flagged changes (e.g., `labs?.membersSigninOTCAlpha`), avoid suggesting modifications to non-flagged code paths unless they're directly related to the PR's objectives. Keep the scope focused on the feature-flag-specific changes only.
📚 Learning: 2025-09-02T12:37:00.421Z
Learnt from: kevinansfield
PR: TryGhost/Ghost#24779
File: ghost/core/core/server/services/members/members-api/members-api.js:238-240
Timestamp: 2025-09-02T12:37:00.421Z
Learning: The otcVerification parameter in Ghost's members API functions like getTokenDataFromMagicLinkToken is optional and only relevant for OTC (one-time code) verification flows. Existing call sites that don't use OTC verification don't need to be updated as the parameter will be undefined, which is the expected behavior.

Applied to files:

  • apps/portal/src/tests/SigninFlow.test.js
  • ghost/core/core/server/services/lib/magic-link/MagicLink.js
🧬 Code graph analysis (6)
ghost/core/test/e2e-api/members/send-magic-link.test.js (8)
ghost/core/core/server/services/members/members-api/controllers/RouterController.js (6)
  • email (133-133)
  • email (535-535)
  • tokenProvider (700-700)
  • require (4-4)
  • require (6-6)
  • redirectUrl (734-734)
ghost/core/test/e2e-api/members/signin.test.js (46)
  • email (121-121)
  • email (151-151)
  • email (164-164)
  • email (180-180)
  • email (233-233)
  • email (365-365)
  • magicLink (51-51)
  • magicLink (62-62)
  • magicLink (73-73)
  • magicLink (84-84)
  • magicLink (103-103)
  • magicLink (122-122)
  • magicLink (153-153)
  • magicLink (165-165)
  • magicLink (195-195)
  • magicLink (247-247)
  • magicLink (300-300)
  • magicLink (342-342)
  • magicLink (366-372)
  • magicLinkUrl (52-52)
  • magicLinkUrl (63-63)
  • magicLinkUrl (74-74)
  • magicLinkUrl (85-85)
  • magicLinkUrl (104-104)
  • magicLinkUrl (123-123)
  • magicLinkUrl (154-154)
  • magicLinkUrl (166-166)
  • magicLinkUrl (196-196)
  • magicLinkUrl (248-248)
  • magicLinkUrl (301-301)
  • magicLinkUrl (343-343)
  • magicLinkUrl (373-373)
  • token (53-53)
  • token (64-64)
  • token (75-75)
  • token (86-86)
  • token (105-105)
  • token (124-124)
  • token (155-155)
  • token (167-167)
  • token (197-197)
  • token (249-249)
  • token (302-302)
  • token (344-344)
  • token (374-374)
  • require (1-1)
ghost/core/core/server/services/members/middleware.js (7)
  • membersService (4-4)
  • token (151-151)
  • token (162-162)
  • token (174-174)
  • require (9-12)
  • redirectUrl (382-382)
  • redirectUrl (399-399)
ghost/core/core/server/services/members/members-api/members-api.js (3)
  • body (2-2)
  • require (1-1)
  • require (17-17)
ghost/core/test/unit/server/services/members/SingleUseTokenProvider.test.js (2)
  • tokenProvider (11-11)
  • otc (333-333)
ghost/core/core/server/services/members/SingleUseTokenProvider.js (2)
  • require (2-2)
  • require (5-5)
ghost/core/core/server/web/shared/middleware/api/spam-prevention.js (1)
  • otcVerification (290-319)
ghost/core/test/utils/e2e-framework.js (1)
  • resetRateLimits (174-178)
apps/portal/src/components/pages/MagicLinkPage.test.js (2)
apps/portal/src/tests/SigninFlow.test.js (2)
  • utils (29-31)
  • utils (85-87)
apps/portal/src/utils/test-utils.js (2)
  • utils (34-34)
  • utils (45-45)
apps/portal/src/tests/SigninFlow.test.js (1)
apps/portal/src/App.js (1)
  • App (45-1045)
ghost/core/core/frontend/helpers/ghost_head.js (1)
ghost/core/core/shared/labs.js (1)
  • settingsCache (13-13)
apps/portal/src/actions.js (1)
ghost/core/test/e2e-api/members/send-magic-link.test.js (7)
  • response (799-800)
  • response (806-807)
  • response (847-848)
  • response (862-863)
  • response (880-881)
  • response (900-901)
  • response (908-909)
apps/portal/src/data-attributes.js (2)
apps/portal/src/components/pages/MagicLinkPage.test.js (1)
  • form (50-50)
apps/portal/src/tests/data-attributes.test.js (1)
  • form (31-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Legacy tests (Node 22.13.1, mysql8)
  • GitHub Check: Legacy tests (Node 22.13.1, sqlite3)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Acceptance tests (Node 22.13.1, mysql8)
  • GitHub Check: Lint
  • GitHub Check: Acceptance tests (Node 22.13.1, sqlite3)
  • GitHub Check: Unit tests (Node 22.13.1)
  • GitHub Check: Admin tests - Chrome
  • GitHub Check: Build & Push
🔇 Additional comments (21)
ghost/core/core/frontend/helpers/ghost_head.js (1)

51-67: LGTM!

The removal of members-signin-otc and members-signin-otc-alpha data attributes is correct. The OTC flow now relies on otc_ref in API responses rather than these frontend flags.

ghost/core/core/server/services/lib/magic-link/MagicLink.js (2)

88-95: LGTM!

The removal of labsService.isSet('membersSigninOTC') check is correct. OTC retrieval is now driven solely by options.includeOTC, aligning with the PR's objective to remove feature flag gating.


109-116: LGTM!

The removal of the labsService.isSet('membersSigninOTC') check is correct. The otcRef is now returned whenever an OTC is successfully derived, independent of feature flags.

apps/portal/src/actions.js (1)

81-111: LGTM!

The changes correctly implement the new OTC flow:

  1. Line 88: Always includes includeOTC: true in signin requests, removing the labs flag dependency.
  2. Line 92: Checks only for response?.otc_ref to determine OTC flow, which is the correct behavior.

This aligns with the PR's objective to make OTC behavior driven by API responses rather than feature flags.

apps/portal/src/components/pages/MagicLinkPage.test.js (3)

7-40: LGTM!

The test setup changes correctly reflect the new OTC flow:

  1. Line 9: Default labs is now {}, removing the previous membersSigninOTC: false flag.
  2. Line 36: setupOTCTest no longer needs labs: {membersSigninOTC: true} and uses labs: {} instead.

Tests now rely solely on otcRef presence to determine OTC behavior, which is the correct approach.


79-99: LGTM!

The test updates correctly reflect the simplified OTC rendering logic:

  1. Line 80: Test renamed to "renders OTC form when otcRef exists" (removed flag reference).
  2. Line 89: Scenarios simplified from multiple flag combinations to a single scenario checking only otcRef: null.

This aligns with the OTC form now being controlled entirely by otcRef presence rather than feature flags.


313-323: LGTM!

The edge case test correctly validates that the OTC form is not rendered when otcRef is null, regardless of labs configuration. Line 316 now uses labs: {} instead of checking flag states.

ghost/core/test/unit/server/services/lib/magic-link/index.test.js (1)

104-210: LGTM!

The test suite updates correctly reflect the new OTC flow:

  1. Line 104: Describe block renamed from "sendMagicLink with OTC flags" to "sendMagicLink with OTC".
  2. Tests now use buildOptions() consistently (lines 106, 125, 145, 164, 189, 213, 239) instead of passing custom labsService configurations.
  3. Tests validate OTC behavior based on includeOTC option (lines 163, 188) rather than labsService flag state.

All changes align with removing feature flag gating from OTC functionality.

ghost/core/test/unit/server/services/members/members-api/controllers/RouterController.test.js (1)

860-930: LGTM!

The test suite updates correctly reflect the new OTC response handling:

  1. Line 860: Describe block renamed from "membersSigninOTC feature flag" to "OTC response handling".
  2. Line 883: labsService.isSet now returns false by default, emphasizing that OTC behavior is no longer flag-dependent.
  3. Lines 904-929: Tests simplified to focus on otcRef presence:
    • Line 904: "should return otc_ref when otcRef exists" (removed flag reference)
    • Line 913: "should not return otc_ref when no otcRef"
    • Line 922: "should not return otc_ref when otcRef is undefined"

All changes align with OTC response behavior being driven by otcRef presence rather than feature flags.

apps/portal/src/tests/SigninFlow.test.js (4)

123-131: LGTM!

The helper function expectOTCEnabledSendMagicLinkAPICall provides a clean way to verify that signin API calls include the required OTC parameters:

  • includeOTC: true
  • emailType: 'signin'
  • integrityToken

This reduces duplication across tests and ensures consistent validation of the OTC-enabled flow.


146-176: LGTM!

The test updates correctly reflect the new OTC flow:

  1. Lines 154-156: sendMagicLink is now mocked to return otc_ref, enabling the OTC flow in the default signin test.
  2. Line 175: Uses the new helper function to validate the OTC-enabled API call.

This aligns with OTC being driven by the presence of otc_ref in responses rather than feature flags.


389-429: LGTM!

The OTC integration flow setup correctly removes feature flag dependencies:

  1. Line 416: Renders App with labs={{}} instead of labs={{membersSigninOTC: true}}.
  2. OTC flow is triggered by sendMagicLink returning otc_ref (lines 399-403) rather than by feature flags.

This aligns with the PR's objective to make OTC behavior response-driven.


450-506: LGTM!

The integration tests correctly use the expectOTCEnabledSendMagicLinkAPICall helper:

  1. Lines 457, 484, 501: Consistent validation of OTC-enabled API calls across different test scenarios.

This ensures all signin flows include includeOTC: true and validates the new OTC behavior.

ghost/core/test/e2e-api/members/send-magic-link.test.js (8)

336-340: LGTM! Good snapshot test for OTC email content.

The test appropriately verifies the email content when OTC is requested, using the scrubEmailContent helper to normalize tokens and OTC codes for consistent snapshots.


792-810: LGTM! Clear tests for OTC response behavior.

These tests effectively verify that otc_ref is returned only when explicitly requested via includeOTC: true, confirming the opt-in nature of the OTC feature.


812-832: LGTM! Comprehensive email content verification.

Good use of helper functions and parameterized tests to verify OTC presence/absence in email content across different email types.


834-843: LGTM! Important regression test.

This test ensures the traditional magic link flow continues to work correctly alongside the new OTC feature.


845-873: LGTM! Thorough edge case testing.

Excellent coverage of type coercion scenarios, verifying that both boolean and string representations of true/false work correctly for the includeOTC parameter.


875-897: LGTM! Excellent error handling coverage.

This test demonstrates robust failure handling—when OTC generation fails, the system gracefully degrades by sending a magic link without OTC rather than failing the entire request. The verification that the stub was called (line 884) ensures the test is actually exercising the failure path.


899-958: LGTM! Comprehensive verification endpoint coverage.

The sendAndVerifyOTC helper (lines 907-929) is well-designed and supports testing various redirect scenarios. The tests cover:

  • Non-existent member rejection (security)
  • Basic verification flow with redirect URL validation
  • Referer header handling
  • Explicit redirect parameter override

960-1081: LGTM! Comprehensive rate limiting coverage.

Excellent test coverage for OTC verification rate limiting:

  • IP-based enumeration limiting prevents brute-force attacks across codes
  • Per-code limiting prevents brute-force attacks on specific codes
  • Independent tracking of different otcRef values

The defensive assertion at line 1049 ensuring otcVerificationLimit < otcVerificationEnumerationLimit is particularly good—it validates the test setup and prevents false positives.

Comment on lines +402 to 436
it('allows signins from email domains blocked in config', async function () {
const email = 'hello-enabled@blocked-domain-config.com';
await membersService.api.members.create({email, name: 'Member Test'});

await membersAgent.post('/api/send-magic-link')
.body({
email,
emailType: 'signin',
includeOTC: true
})
.expectStatus(201)
.expect(({body}) => {
Object.keys(body).should.eql(['otc_ref']);
body.otc_ref.should.be.a.String().and.match(/^[a-f0-9-]{36}$/);
});
});

it('allows signins from email domains blocked in settings', async function () {
settingsCache.set('all_blocked_email_domains', {value: ['blocked-domain-setting.com']});

const email = 'hello-enabled@blocked-domain-setting.com';
await membersService.api.members.create({email, name: 'Member Test'});

await membersAgent.post('/api/send-magic-link')
.body({
email,
emailType: 'signin',
includeOTC: true
})
.expectStatus(201)
.expect(({body}) => {
should.exist(body.otc_ref);
body.otc_ref.should.be.a.String().and.match(/^[a-f0-9-]{36}$/);
});
});
await membersAgent.post('/api/send-magic-link')
.body({
email,
emailType: 'signin',
includeOTC: true
})
.expectStatus(201)
.expect(({body}) => {
Object.keys(body).should.eql(['otc_ref']);
body.otc_ref.should.be.a.String().and.match(/^[a-f0-9-]{36}$/);
});
});

describe('with membersSigninOTC feature flag disabled', function () {
beforeEach(function () {
settingsCache.set('labs', {value: JSON.stringify({membersSigninOTC: false})});
});
it('allows signins from email domains blocked in settings', async function () {
settingsCache.set('all_blocked_email_domains', {value: ['blocked-domain-setting.com']});

it('allows signins from email domains blocked in config', async function () {
const email = 'hello-disabled@blocked-domain-config.com';
await membersService.api.members.create({email, name: 'Member Test'});
const email = 'hello-enabled@blocked-domain-setting.com';
await membersService.api.members.create({email, name: 'Member Test'});

await membersAgent.post('/api/send-magic-link')
.body({
email,
emailType: 'signin'
})
.expectEmptyBody()
.expectStatus(201);
});

it('allows signins from email domains blocked in settings', async function () {
settingsCache.set('all_blocked_email_domains', {value: ['blocked-domain-setting.com']});

const email = 'hello-disabled@blocked-domain-setting.com';
await membersService.api.members.create({email, name: 'Member Test'});

await membersAgent.post('/api/send-magic-link')
.body({
email,
emailType: 'signin'
})
.expectEmptyBody()
.expectStatus(201);
});
await membersAgent.post('/api/send-magic-link')
.body({
email,
emailType: 'signin',
includeOTC: true
})
.expectStatus(201)
.expect(({body}) => {
should.exist(body.otc_ref);
body.otc_ref.should.be.a.String().and.match(/^[a-f0-9-]{36}$/);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Inconsistent response body assertions.

Lines 414-415 use a strict assertion expecting the response body to contain only otc_ref:

Object.keys(body).should.eql(['otc_ref']);

Lines 433-434 use a lenient assertion checking only that otc_ref exists:

should.exist(body.otc_ref);

Both tests verify similar scenarios (signin with blocked domains and includeOTC: true). The inconsistency suggests unclear expectations about the response structure.

Standardize the assertions. If the response should contain only otc_ref, use the strict assertion in both tests. If other fields are acceptable, use the lenient assertion consistently:

Option 1 (strict):

 .expect(({body}) => {
+    Object.keys(body).should.eql(['otc_ref']);
-    should.exist(body.otc_ref);
     body.otc_ref.should.be.a.String().and.match(/^[a-f0-9-]{36}$/);
 });

Option 2 (lenient):

 .expect(({body}) => {
-    Object.keys(body).should.eql(['otc_ref']);
+    should.exist(body.otc_ref);
     body.otc_ref.should.be.a.String().and.match(/^[a-f0-9-]{36}$/);
 });
🤖 Prompt for AI Agents
In ghost/core/test/e2e-api/members/send-magic-link.test.js around lines 402-436,
the two tests use inconsistent response assertions (one checks
Object.keys(body).should.eql(['otc_ref']) while the other uses
should.exist(body.otc_ref)); standardize them to the strict expectation: replace
the lenient should.exist(body.otc_ref) assertion in the second test with the
strict assertion Object.keys(body).should.eql(['otc_ref']) and keep the existing
otc_ref format check so both tests assert the response contains only otc_ref and
that it matches the UUID-like pattern.

closes https://linear.app/ghost/issue/BER-2520/

- Portal's sign-in flow will now include a one-time-code (OTC) in emails and display a code input form after submitting the standard email sign-in form
  - OTC usage is optional for members, magic links are still included alongside the code
  - reduces sign-in friction in scenarios such as in-app browsers
- custom sign-in forms can opt in to the same OTC flow by adding a `data-members-otc=true"`
  - in this scenario, Portal will open the code input modal once a member submits their email in the custom form
@kevinansfield kevinansfield merged commit a08197f into main Oct 22, 2025
32 checks passed
@kevinansfield kevinansfield deleted the release-members-OTC branch October 22, 2025 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community [triage] Community features and bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant