Skip to content

Commit a62dfc5

Browse files
9larsonsrob-ghost
authored andcommitted
🐛 Fixed newsletter reply-to verification links failing after signin
ref https://linear.app/ghost/issue/ONC-1658/ - when a customer clicks a newsletter reply-to verification email in a browser that isn't signed in to admin, the link sends them through the signin redirect introduced in #27316 — but the post-signin replay used router.transitionTo, which drops query params not declared on the resolved route's controller - react-fallback is a wildcard route with no queryParams config, so the ?verifyEmail=<token> param was silently stripped on replay; the React verify-on-mount handler in newsletters.tsx then no-ops because the token is missing, and the customer thinks the address didn't save (root cause for ONC-1618 and ONC-1642) - swap to windowProxy.replaceLocation, which is already the auth-flow primitive used in authenticated.js and application.js — the hard navigation feeds the URL straight to the browser and bypasses Ember's URL-rebuilding entirely - e2e covers the round-trip; verified to fail on unfixed code
1 parent caca37d commit a62dfc5

2 files changed

Lines changed: 30 additions & 1 deletion

File tree

e2e/tests/admin/signin.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,27 @@ test.describe('Ghost Admin - Signin Redirect', () => {
3535

3636
await postsPage.waitForPageToFullyLoad();
3737
});
38+
39+
test('query params on a deep link survive signin redirect', async ({page, ghostAccountOwner}) => {
40+
// Newsletter reply-to verification emails point at
41+
// /settings/newsletters/?verifyEmail=<token>. If the user clicks the
42+
// link in a browser that isn't signed in, the param needs to survive
43+
// the signin round-trip, otherwise the verify-on-mount handler in
44+
// newsletters.tsx no-ops and the customer thinks their reply-to
45+
// address didn't save (ONC-1618 / ONC-1642).
46+
await logout(page);
47+
48+
await page.goto('/ghost/#/settings/newsletters/?verifyEmail=fake-token-xyz');
49+
50+
const loginPage = new LoginPage(page);
51+
await expect(loginPage.signInButton).toBeVisible();
52+
53+
await loginPage.signIn(ghostAccountOwner.email, ghostAccountOwner.password);
54+
55+
// The error modal is the signal that the React verify handler ran:
56+
// it only renders when newsletters.tsx attempted to redeem a token
57+
// and the API rejected it (expected, since the token is fake).
58+
await expect(page.getByRole('heading', {name: 'Error verifying email address'})).toBeVisible();
59+
expect(page.url()).toContain('verifyEmail=fake-token-xyz');
60+
});
3861
});

ghost/admin/app/services/session.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import AuthConfiguration from 'ember-simple-auth/configuration';
12
import ESASessionService from 'ember-simple-auth/services/session';
23
import RSVP from 'rsvp';
4+
import windowProxy from 'ghost-admin/utils/window-proxy';
35
import {configureScope} from '@sentry/ember';
46
import {getOwner} from '@ember/application';
57
import {inject} from 'ghost-admin/decorators/inject';
@@ -91,7 +93,11 @@ export default class SessionService extends ESASessionService {
9193
const redirectUrl = window.sessionStorage.getItem('ghost-signin-redirect');
9294
window.sessionStorage.removeItem('ghost-signin-redirect');
9395
if (redirectUrl && !redirectUrl.startsWith('/signin') && !redirectUrl.startsWith('/signup') && !redirectUrl.startsWith('/setup')) {
94-
this.router.transitionTo(redirectUrl);
96+
// Hard navigate rather than router.transitionTo: the catch-all
97+
// react-fallback route has no controller-declared queryParams,
98+
// so transitionTo strips params like ?verifyEmail=<token> used
99+
// by newsletter reply-to confirmation links.
100+
windowProxy.replaceLocation(`${AuthConfiguration.rootURL}#${redirectUrl}`);
95101
return;
96102
}
97103

0 commit comments

Comments
 (0)