Skip to content

🐛 Fixed deep-linking to Admin screens whilst logged out#27316

Merged
kevinansfield merged 3 commits intomainfrom
fix-unauthenticated-react-route-redirect
Apr 9, 2026
Merged

🐛 Fixed deep-linking to Admin screens whilst logged out#27316
kevinansfield merged 3 commits intomainfrom
fix-unauthenticated-react-route-redirect

Conversation

@kevinansfield
Copy link
Copy Markdown
Member

@kevinansfield kevinansfield commented Apr 9, 2026

ref https://linear.app/ghost/issue/ONC-1623/

When deep-linking to an admin route (e.g. /ghost/#/tags) while logged out, the page rendered blank instead of showing the signin screen. This was caused by the react-fallback catch-all route lacking the authentication check that was previously on the now-removed Ember route files.

  • Added react-fallback route extending AuthenticatedRoute to enforce signin for all React-rendered routes
  • Persisted intended URL in sessionStorage before the auth redirect so the user is returned to their original destination after signing in (works for both React and Ember routes)
  • Made LoginPage.logout() wait for the signin page to fully load before resolving to avoid race conditions with subsequent navigations in e2e tests
  • Added e2e tests covering signin redirect for both React (/tags) and Ember (/posts) routes

ref https://linear.app/ghost/issue/BER-3337/

When deep-linking to an admin route (e.g. /ghost/#/tags) while logged
out, the page rendered blank instead of showing the signin screen. This
was caused by the react-fallback catch-all route lacking the
authentication check that was previously on the now-removed Ember route
files.

- Added react-fallback route extending AuthenticatedRoute
- Persisted intended URL in sessionStorage before the auth redirect so
  the user is returned to their original destination after signing in
- Made LoginPage.logout() wait for the signin page to fully load before
  resolving to avoid race conditions with subsequent navigations
- Added e2e tests covering signin redirect for both React and Ember
  routes
@kevinansfield kevinansfield requested a review from 9larsons as a code owner April 9, 2026 17:59
@kevinansfield kevinansfield changed the title 🐛 Fixed unauthenticated redirect for React and Ember admin routes 🐛 Fixed deep-linking to Admin screens Apr 9, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3d859e1c-c5c4-4a1a-8aca-88977d137bca

📥 Commits

Reviewing files that changed from the base of the PR and between e73ceff and d2211ff.

📒 Files selected for processing (1)
  • ghost/admin/app/routes/authenticated.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/admin/app/routes/authenticated.js

Walkthrough

AuthenticatedRoute.beforeModel now stores the attempted URL in window.sessionStorage under ghost-signin-redirect when unauthenticated and removes it when authenticated. session.handleAuthentication reads and clears that key after sign-in and redirects to the stored URL unless it begins with /signin, /signup, or /setup, otherwise falling back to home. A ReactFallbackRoute subclass was added. E2E/test and helper updates: logout now waits for sign-in visibility, PostsPage gained waitForPageToFullyLoad(), new Playwright signin-redirect tests, and an acceptance test expectation updated to /signin.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix: resolving an issue where deep-linking to admin screens while logged out rendered blank pages instead of showing signin.
Description check ✅ Passed The description is directly related to the changeset, explaining both the bug being fixed and the implementation approach across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-unauthenticated-react-route-redirect

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

@kevinansfield kevinansfield enabled auto-merge (squash) April 9, 2026 18:02
@kevinansfield kevinansfield changed the title 🐛 Fixed deep-linking to Admin screens 🐛 Fixed deep-linking to Admin screens whilst logged out Apr 9, 2026
@kevinansfield kevinansfield disabled auto-merge April 9, 2026 18:03
@kevinansfield kevinansfield enabled auto-merge (squash) April 9, 2026 18:03
Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
e2e/helpers/pages/admin/posts/posts-page.ts (1)

47-47: Consider an explicit return type for consistency.

waitForPageToFullyLoad() could be declared as async waitForPageToFullyLoad(): Promise<void> to match the surrounding method style.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/helpers/pages/admin/posts/posts-page.ts` at line 47, The method
waitForPageToFullyLoad is missing an explicit return type; update its signature
to include Promise<void> (declare async waitForPageToFullyLoad(): Promise<void>)
to match surrounding methods and improve type consistency, ensuring any callers
still work and no return values are added.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@e2e/helpers/pages/admin/posts/posts-page.ts`:
- Line 47: The method waitForPageToFullyLoad is missing an explicit return type;
update its signature to include Promise<void> (declare async
waitForPageToFullyLoad(): Promise<void>) to match surrounding methods and
improve type consistency, ensuring any callers still work and no return values
are added.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6bcfdaed-15ae-4628-9593-71f1f4c70ff9

📥 Commits

Reviewing files that changed from the base of the PR and between de75491 and 862bea9.

📒 Files selected for processing (6)
  • e2e/helpers/pages/admin/login-page.ts
  • e2e/helpers/pages/admin/posts/posts-page.ts
  • e2e/tests/admin/signin.test.ts
  • ghost/admin/app/routes/authenticated.js
  • ghost/admin/app/routes/react-fallback.js
  • ghost/admin/app/services/session.js

- Always clear ghost-signin-redirect from sessionStorage after reading
- Filter out /signin, /signup, /setup URLs from redirect targets
- Updated authentication test expectation for react-fallback auth check
Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ghost/admin/app/services/session.js`:
- Around line 91-96: The early return bypasses ember-simple-auth's cleanup; call
super.handleAuthentication('home') before performing the custom redirect so ESA
can clear attemptedTransition and redirect targets. Concretely: after reading
and removing 'ghost-signin-redirect' (redirectUrl and
window.sessionStorage.removeItem) but before router.transitionTo(redirectUrl)
and return, invoke and await super.handleAuthentication('home') (or call
super.handleAuthentication if not async) so ESA's internal cleanup runs, then
proceed with this.router.transitionTo(redirectUrl) and return.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 87030c2f-45b0-44e5-81a5-528dcb20a645

📥 Commits

Reviewing files that changed from the base of the PR and between 862bea9 and e73ceff.

📒 Files selected for processing (2)
  • ghost/admin/app/services/session.js
  • ghost/admin/tests/acceptance/authentication-test.js

Comment on lines +91 to +96
const redirectUrl = window.sessionStorage.getItem('ghost-signin-redirect');
window.sessionStorage.removeItem('ghost-signin-redirect');
if (redirectUrl && !redirectUrl.startsWith('/signin') && !redirectUrl.startsWith('/signup') && !redirectUrl.startsWith('/setup')) {
this.router.transitionTo(redirectUrl);
return;
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In the ember-simple-auth version used by Ghost admin, does SessionService.handleAuthentication clear attemptedTransition (or equivalent state), and what side effects are skipped if an override returns before calling super.handleAuthentication()?

💡 Result:

Ghost Admin pins ember-simple-auth to 5.0.0 in ghost/admin/package.json. [1]

Does SessionService.handleAuthentication clear attemptedTransition (or equivalent)?

In ember-simple-auth, SessionService.handleAuthentication(routeAfterAuthentication) does not directly clear state itself—it delegates to an internal helper (handleSessionAuthenticated(...)). [2]

That helper:

  • retries attemptedTransition if present, then clears it via sessionService.set('attemptedTransition', null), or
  • if there’s no attemptedTransition but there is a persisted redirect target, it transitions there and clears the redirect target, otherwise
  • transitions to routeAfterAuthentication. [3]

So: yes, the “attempted transition” is cleared, but it’s cleared inside the internal handleSessionAuthenticated flow that handleAuthentication calls. [2][3]

If an override returns before calling super.handleAuthentication(), what side effects are skipped?

You skip all of the default post-login routing/cleanup behavior, specifically: [2][3]

  • No attemptedTransition.retry()
  • No attemptedTransition reset to null (so it can remain stale in memory)
  • No redirect to the persisted redirect target (if set)
  • No clearRedirectTarget() (so the stored redirect target can persist and affect later navigations)
  • No fallback transition to routeAfterAuthentication (so you may remain on the login route unless you handle navigation yourself)

Bottom line: returning early means you must perform the retry/redirect + cleanup yourself, or you risk stale redirect state and “wrong” redirects later. [3]

Sources

[1] Ghost Admin package.json (Ghost v5.84.2) showing ember-simple-auth: "5.0.0". (github.com)
[2] ember-simple-auth SessionService.handleAuthentication delegates to handleSessionAuthenticated. (ember-simple-auth.com)
[3] ember-simple-auth internal handleSessionAuthenticated retries & clears attemptedTransition, and clears redirect target when used. (github.com)

Citations:


🏁 Script executed:

# First, find the session.js file
find . -type f -name "session.js" | grep -E "ghost/admin"

Repository: TryGhost/Ghost

Length of output: 1867


Preserve ESA transition-state cleanup when taking the custom redirect path.

Line 95 returns before super.handleAuthentication('home'). In ember-simple-auth v5.0.0, handleAuthentication() triggers internal cleanup that clears attemptedTransition and the redirect target. By returning early, this cleanup is bypassed, leaving stale transition state that can cause unexpected redirects on future auth events.

Suggested patch
 const redirectUrl = window.sessionStorage.getItem('ghost-signin-redirect');
 window.sessionStorage.removeItem('ghost-signin-redirect');
 if (redirectUrl && !redirectUrl.startsWith('/signin') && !redirectUrl.startsWith('/signup') && !redirectUrl.startsWith('/setup')) {
+    // Avoid leaving stale ESA transition state when bypassing super.handleAuthentication()
+    this.attemptedTransition = null;
     this.router.transitionTo(redirectUrl);
     return;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/admin/app/services/session.js` around lines 91 - 96, The early return
bypasses ember-simple-auth's cleanup; call super.handleAuthentication('home')
before performing the custom redirect so ESA can clear attemptedTransition and
redirect targets. Concretely: after reading and removing 'ghost-signin-redirect'
(redirectUrl and window.sessionStorage.removeItem) but before
router.transitionTo(redirectUrl) and return, invoke and await
super.handleAuthentication('home') (or call super.handleAuthentication if not
async) so ESA's internal cleanup runs, then proceed with
this.router.transitionTo(redirectUrl) and return.

@kevinansfield kevinansfield disabled auto-merge April 9, 2026 19:31
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 9, 2026

@kevinansfield kevinansfield merged commit 75ec35f into main Apr 9, 2026
36 checks passed
@kevinansfield kevinansfield deleted the fix-unauthenticated-react-route-redirect branch April 9, 2026 21:32
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.

1 participant