Skip to content

feat(sc-44544): auto opt-in to Library Assistant after promo-driven auth#3399

Merged
yodem merged 5 commits into
feature/sc-44545/update-login-page-to-handle-anon-la-candidatesfrom
feature/sc-44544/la-promo-change-opt-in-behavior-for-non
Jun 15, 2026
Merged

feat(sc-44544): auto opt-in to Library Assistant after promo-driven auth#3399
yodem merged 5 commits into
feature/sc-44545/update-login-page-to-handle-anon-la-candidatesfrom
feature/sc-44544/la-promo-change-opt-in-behavior-for-non

Conversation

@yodem

@yodem yodem commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

What & why

Closes the last manual step in the Library Assistant promo flow for non-authenticated users.

Before: an anon user who clicked the promo CTA had to (1) log in / register, then (2) click a second Join the Experiment button before the assistant appeared.

After: clicking the promo CTA → logging in or registering → the user is automatically enrolled in the experiments whitelist and returned to where they were, with the Library Assistant showing on reload. No extra click. (A page reload is expected and acceptable.)

How

The promo's login/register ?next= is routed through a new /enable-library-assistant landing view:

  1. Promo CTA → /login?next=/enable-library-assistant?next=<originalPath> (same for /register).
  2. After auth, Django redirects to the landing view.
  3. The view enrolls the user via the existing _set_user_experiments(...) helper, then redirects back to <originalPath>.
  4. Server-side render now sees the user in the experiment → assistant appears; promo is gone.

This reuses the ?next= plumbing the login page already hardened, so it survives the login↔register cross-flow switch and the form POST without extra wiring.

Safety / edges

  • Redirect target is same-host validated (open-redirect guard), same pattern as CustomLogoutView.
  • Anonymous hits on the landing view bounce to login (the promo always routes through auth first).
  • The register welcome=to-sefaria flag is forwarded to the final destination.
  • Normal (non-promo) logins are unaffected — only CTAs that route through the landing view enroll.
  • Idempotent: re-enrolling fires no duplicate CRM webhook.

Tests

reader/tests/enable_library_assistant_test.py: enroll+redirect, anon→login (not enrolled), offsite-next fallback, welcome forwarding, missing-next default, idempotency.

Base branch

Targets the login-page branch (the cross-flow nav it depends on) — not yet merged to master. Rebase to master once that lands.

🤖 Generated with Claude Code

yodem and others added 2 commits June 10, 2026 10:46
Anon users who click the Library Assistant promo CTA were enrolled in
the experiment only after a second 'Join' click post-login. Route the
promo's login/register ?next= through a new /enable-library-assistant
landing view: once auth completes the user lands there, is enrolled in
the experiments whitelist (reusing _set_user_experiments), and is
redirected back to where they were. The assistant then appears on
reload with no extra click.

- New view enable_library_assistant: same-host-validated redirect,
  forwards the register welcome flag, bounces anon hits to login.
- Banner anon CTAs now target the opt-in landing via nested ?next=.
- Normal (non-promo) logins are unaffected.
- Tests cover enroll+redirect, anon->login, offsite-next fallback,
  welcome forwarding, default, and idempotency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Source files shouldn't carry Shortcut story IDs (branch/commit refs are
enough). Replicated on the login-page branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gitvelocity-reviewer

Copy link
Copy Markdown

📊 Code Quality Score: 18/100

44 × 0.4 = 17.6 → 18

Category Score Factors
🔭 Scope 9/20 5 files across JSX, views, URLs, tests, CSS; single feature touching frontend + backend + routing
🏗️ Architecture 7/20 New URL endpoint and view function; follows existing patterns; no new service dependencies
⚙️ Implementation 8/20 Open-redirect protection, URL manipulation with query string forwarding, double-hop redirect chain, idempotency handling
⚠️ Risk 6/20 Auth flow routing change; open-redirect mitigation present; low blast radius; no DB migration
✅ Quality 11/15 6 test cases covering happy path, anon redirect, SSRF/open-redirect, welcome param, missing next, idempotency; minor assertion scope issue in anon test
🔒 Perf / Security 3/5 Explicit url_has_allowed_host_and_scheme validation; no rate limiting; no CSRF decorator on state-mutating GET

Was this score accurate? 👍 Yes · 👎 No

Scored by GitVelocity · How are scores calculated?

Skip Library Assistant enrollment when the browser reports a cross-site
request (Sec-Fetch-Site), so a cross-site <img> can't silently enroll a
logged-in user; the real same-origin post-login redirect is unaffected.
Add tests for protocol-relative next, welcome appended to an existing
query string, and the cross-site skip; assert no duplicate settings row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@yodem

yodem commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

/ork multi-agent review — verdict: COMMENT → addressed

Ran a 3-agent review (security/auth-flow, correctness/integration, test adequacy). No blockers; the feature was verified end-to-end (login + register paths, nested ?next= encoding, LA appearance on the return render, no regression to the logged-in "Join" button or normal login).

Findings addressed in 162aae36a:

  • Security (medium): CSRF-on-GET state mutation. The view enrolled (and fired the CRM webhook) on a GET, so a cross-site <img src=".../enable-library-assistant"> could silently enroll a logged-in user. Added a Sec-Fetch-Site: cross-site guard that skips enrollment for cross-site subresource loads while leaving the real same-origin post-login redirect (and header-less clients) untouched.
  • Tests: added protocol-relative //evil.com fallback, welcome appended onto an existing query string, the cross-site skip, and a no-duplicate-row assertion on the idempotency test.

Noted, not changed (out of scope / pre-existing):

  • register() redirects to POST['next'] without host validation — pre-existing, separate from this PR.
  • _set_user_experiments saves the Mongo profile even when unchanged — minor, pre-existing.
  • The cross-flow nav renders ?next= without urlencoding — pre-existing behavior from feat(sc-44545): make registration discoverable on login page #3370, already flagged there as a follow-up.

🤖 review run via ork-style parallel agents

@yodem

yodem commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

Verification note — CI PyTest red is a suite-wide 60-min timeout, not this PR

The Continuous Testing: PyTest check shows FAILURE, but the cause is The action 'Wait For Job To Finish' has timed out after 60 minutes — the full suite only reached ~22% before the wall-clock limit. The only FAILED tests in the log are sefaria/helper/tests/auto_linking_test.py::Test_AutoLinker::test_refresh_links_with_text_save*, which are unrelated to this change (auto-linker), and this PR's tests under reader/tests/ never got a chance to run.

To verify this PR's tests directly, I ran the new file inside the sc-44544-promo cauldron pod (full Mongo/Postgres stack, confirmed running this branch's latest image incl. the Sec-Fetch-Site guard):

python -m pytest reader/tests/enable_library_assistant_test.py -v
9 passed in 13.27s

All 9 cases green: enroll+redirect, anon→login (no enroll), offsite-next fallback, protocol-relative //evil.com fallback, welcome forwarding (incl. existing query string), cross-site-GET skip, missing-next default, idempotency.

Also verified live on the cauldron: logged-out GET /enable-library-assistant?next=/Genesis.1 → 302 to login with the nested next intact; the deployed client bundle routes the promo CTAs through /enable-library-assistant; login & register both preserve the opt-in next across the cross-flow switch.

@yodem yodem requested a review from Copilot June 15, 2026 06:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@yodem yodem requested a review from stevekaplan123 June 15, 2026 07:07
…ndidates' into feature/sc-44544/la-promo-change-opt-in-behavior-for-non
@yodem yodem merged commit 717541b into feature/sc-44545/update-login-page-to-handle-anon-la-candidates Jun 15, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants