Skip to content

fix(newsletters): correct stale project context on deep links (#1570) - #2224

Open
audigregorie wants to merge 8 commits into
mainfrom
fix/gh-1570-newsletter-wrong-ctx
Open

fix(newsletters): correct stale project context on deep links (#1570)#2224
audigregorie wants to merge 8 commits into
mainfrom
fix/gh-1570-newsletter-wrong-ctx

Conversation

@audigregorie

@audigregorie audigregorie commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Newsletter edit and analytics pages could show — and authorize against — the wrong project when opened from a deep link. The URL carries the owning project as :projectUid, but both the access guard and the page chrome (name, logo, sidebar) followed the ?project= query param or a cookie-restored active context, which can be stale when a link is shared or the user switched projects since it was created. This PR makes the route's :projectUid authoritative for both authorization and page context, with the legacy slug chain kept as a fallback.

Resolves #1570

Behavior changes

Newsletter edit/analytics access check

Before After
Opening a newsletter edit or analytics link could wrongly deny access (or check permissions against a different project) if the saved context pointed at another project than the one in the link. Access is checked against the project identified by the link itself, so a legitimate manager is never denied because of a stale saved context; the old behavior only applies when the link doesn't identify a project or it no longer exists.

Project shown on newsletter edit/analytics pages

Before After
The page header and sidebar could display a different project than the newsletter actually being viewed or edited, when the saved context was stale. The displayed project automatically switches to the one the link belongs to, so the page chrome always matches the newsletter on screen.

Newsletter list/create access denial

Before After
A user denied on the newsletter list or create route landed silently back on the overview page. The denial shows an "Access Denied" toast explaining why, matching the convention other guarded pages already use.

Technical changes

apps/lfx-one/src/app/shared/guards/newsletter-access.guard.ts — Newsletter access guard

  • Resolution order is now: route :projectUid param first (own paramMap, then firstChild for the lens-mount invocation), then the legacy ?project= → active-context slug chain.
  • When :projectUid resolves, writer access is checked directly on the resolved project; denial redirects to the lens-appropriate overview with the resolved slug and the _notice: 'access' query param (mirroring writerGuard's toast convention).
  • A null uid lookup (deleted/unknown project) degrades to the legacy slug chain instead of denying on a fetch error. Legacy-chain denials (list/create routes) now also carry _notice: 'access', surfacing the same "Access Denied" toast as writerGuard instead of redirecting silently.

apps/lfx-one/src/app/shared/services/project.service.ts — Project service

  • getProject no longer caches the null produced by its error path: a failed lookup is evicted so the next caller retries instead of replaying a transient error for the rest of the session (review feedback — a poisoned entry could pin the guard and route reconciliation to a stale context).

apps/lfx-one/src/app/shared/utils/entity-project-context.util.ts — Shared context reconciliation

  • New reconcileRouteProjectContext helper: the uid-only variant of the existing entity-signal syncs, for routes whose URL carries the owning project as :projectUid but have no enriched entity payload.
  • Computes a mismatch over both the route uid and activeContextUid() so the correction self-heals after later route-lens re-asserts, without NavigationEnd wiring; re-applies hit the shareReplay-cached getProject (the guard already resolved the same uid), so the happy path costs no extra request.
  • A failed uid lookup resolves null and leaves the existing context untouched; ?project= is only written to the URL when already present, mirroring syncEntityProjectContext.
  • The repeat suppression now requires URL agreement too: a stale ?project= falls through so the NavigationEnd re-apply repairs it once no navigation is in flight (review feedback — activation-time applies can't repair the URL, and suppressing on context alone left the old slug for the session).

apps/lfx-one/src/app/modules/newsletters/newsletter-manage/newsletter-manage.component.ts, apps/lfx-one/src/app/modules/newsletters/newsletter-analytics/newsletter-analytics.component.ts — Newsletter pages

  • Both pages call reconcileRouteProjectContext once from the constructor so displayName/logoUrl/sidebar follow the route's project instead of a stale cookie-restored context.

apps/lfx-one/src/app/shared/guards/newsletter-access.guard.spec.ts — Guard unit tests

  • New suite covering: ED fast path, :projectUid authorization over stale query param/context, child-snapshot resolution at the lens mount, null-lookup degradation to the legacy chain, denial with resolved slug + access notice, legacy-chain preference order, fail-closed lookup failure, and lens-appropriate overview redirects.

apps/lfx-one/e2e/project-context-deep-link.spec.ts — E2E tests

- newsletter-access.guard: resolve the route's :projectUid (own paramMap,
  then the child snapshot at the lens mounts) and authorize against that
  project; fall back to ?project= then active context only when the param
  is absent or the uid lookup fails
- newsletter-manage/analytics: reconcile active context from routeProjectUid
  via applyEntityProjectContext + computeIsFoundation so page chrome stops
  showing a stale project over the route's data
- e2e: newsletter edit deep-link cases (context switch + guard
  authorization) in project-context-deep-link.spec.ts

Signed-off-by: Audi Young <audi.mycloud@gmail.com>
Signed-off-by: Audi Young <audi.mycloud@gmail.com>
Copilot AI balanced review requested due to automatic review settings September 7, 2026 15:51
@audigregorie
audigregorie requested a review from a team as a code owner September 7, 2026 15:51
@cursor

cursor Bot commented Sep 7, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes newsletter authorization and global project-context behavior on deep links; mistakes could wrongly allow or deny access or flash wrong chrome, though fallbacks and new tests mitigate this.

Overview
Fixes stale project context on newsletter edit and analytics deep links: the URL’s :projectUid now drives access checks and page chrome instead of a cookie-restored or ?project= context that can disagree with the link.

newsletterAccessGuard resolves the owning project from the route param (including on the lens mount via the child snapshot), checks writer on that project, and falls back to the legacy ?project= → active-context slug chain when the uid lookup fails. Executive-director deep links await the same cached getProject lookup so chrome does not paint stale context before reconciliation. Denials use _notice: 'access' like other guards.

reconcileRouteProjectContext (new shared helper) loads the route project by uid, updates lens kind and context slots, and re-applies on NavigationEnd so stepper ?step= navigations are not undone by layout lens re-assert. Newsletter manage and analytics call it from their constructors.

ProjectService.getProject evicts cache entries when a fetch resolves to null, so a transient failure cannot pin guards/reconciliation for the session.

Coverage adds guard and util unit tests, Playwright cases for edit/analytics/guard with stale cookies, and monorepo Vitest config scoping so root/package test runs do not pick up Angular specs.

Reviewed by Cursor Bugbot for commit b05b4b9. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Essentials

Run ID: f34eb744-5141-4a01-88b0-fd66416f50a2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

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.

Pull request overview

Fixes newsletter deep links so authorization and page context follow the route’s project.

Changes:

  • Prioritizes :projectUid in the newsletter access guard.
  • Reconciles active project context on edit and analytics pages.
  • Adds guard unit tests and deep-link E2E coverage.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
entity-project-context.util.ts Adds route-project context reconciliation.
newsletter-access.guard.ts Authorizes against the route project first.
newsletter-access.guard.spec.ts Tests guard resolution and redirects.
newsletter-manage.component.ts Reconciles context on edit pages.
newsletter-analytics.component.ts Reconciles context on analytics pages.
project-context-deep-link.spec.ts Tests stale-context newsletter deep links.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/lfx-one/src/app/shared/utils/entity-project-context.util.ts Outdated
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

🚀 Deployment Status

Your branch has been deployed to: https://ui-pr-2224.dev.v2.cluster.linuxfound.info

Deployment Details:

  • Environment: Development
  • Namespace: ui-pr-2224
  • ArgoCD App: ui-pr-2224

The deployment will be automatically removed when this PR is closed.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread apps/lfx-one/src/app/shared/utils/entity-project-context.util.ts Outdated
#1570)

Signed-off-by: Audi Young <audi.mycloud@gmail.com>
Copilot AI review requested due to automatic review settings September 7, 2026 16:24

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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

…-1570)

Signed-off-by: Audi Young <audi.mycloud@gmail.com>
Copilot AI review requested due to automatic review settings September 7, 2026 16:35

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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

apps/lfx-one/src/app/shared/guards/newsletter-access.guard.ts:56

  • The legacy list/create path now adds _notice=access on denial, whereas it previously redirected silently with only project. That changes visible behavior on routes without :projectUid, contradicting #1570’s success criterion that create and list pages behave exactly as before. Keep the notice on the route-UID denial branch, but preserve the old query params in checkWriterAccess and update its legacy test expectation.

Comment thread apps/lfx-one/src/app/shared/utils/entity-project-context.util.ts
Signed-off-by: Audi Young <audi.mycloud@gmail.com>
Copilot AI review requested due to automatic review settings September 7, 2026 17:11

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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

apps/lfx-one/src/app/shared/utils/entity-project-context.util.ts:198

  • No test covers the full-context comparison added here: all new stale-cookie cases use a different UID. A regression back to UID-only equality would therefore pass while a cookie with the correct UID but stale name/logo still renders outdated chrome—the exact edge case this branch handles. Add a case with the same UID and stale metadata and assert the resolved project replaces it.
      if (
        projectContextService.activeRouteLensKind() === (resolved.isFoundation ? 'foundation' : 'project') &&
        isSameProjectContext(projectContextService.activeContext(), resolved.context)
      ) {

Comment thread apps/lfx-one/src/app/shared/utils/entity-project-context.util.ts

@audigregorie audigregorie left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Code Review Summary

Solid, well-scoped fix: the route's :projectUid is now authoritative for both authorization (guard) and page chrome (reconciliation), with the legacy slug chain kept as a documented fallback. I verified the subtle claims rather than taking them on faith — the mount-level firstChild resolution matches app.routes.ts, the guard/page share the getProject(uid, false) shareReplay cache (no extra request), _notice: 'access' is whitelisted in AppComponent, and the e2e stubs genuinely satisfy computeIsFoundation and Playwright's reverse-registration ordering. Local checks: yarn check-types ✅, targeted ESLint ✅, new guard spec 13/13 ✅; CI all green.

One Minor thread below (comment convention in the new e2e case). Nothing blocking.

What's done well

  • Degradation philosophy documented at every decision point (null uid → legacy chain instead of denial; relation-gated null → context untouched; ED fail-open) — comments explain why, not just what.
  • Layered tests: 13 guard unit tests covering every branch, plus e2e cases for both pages with the deliberate "analytics regression would pass edit-route cases" separation.
  • The comment claims hold up under verification — mount-level guard invocation shape, cache sharing, MainLayout re-assert ordering, and the negative-assertion waitForTimeout pattern all match the surrounding codebase.

Comment thread apps/lfx-one/e2e/project-context-deep-link.spec.ts
Signed-off-by: Audi Young <audi.mycloud@gmail.com>
Copilot AI review requested due to automatic review settings September 7, 2026 18:20

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Signed-off-by: Audi Young <audi.mycloud@gmail.com>
Copilot AI review requested due to automatic review settings September 7, 2026 20:11

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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

luismoriguerra
luismoriguerra previously approved these changes Sep 7, 2026

@luismoriguerra luismoriguerra 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.

Approved — no blocking findings. Reviewed 101d7f56bda4.

Build, format:check, lint:check and shared type-check passed; all 6,051 existing tests passed. Browser E2E not re-run (credentials absent).

Non-blocking: cached-null UID fallback retains stale context; list/create now show an access toast; matching-context suppression can leave a stale ?project= (targeted helper reproduction). Details inline. Merge readiness: confirm the fallback/toast scope exceptions to #1570.

Nit (summary only): update the body’s “without NavigationEnd wiring” and “legacy chain otherwise unchanged” claims.

Comment thread apps/lfx-one/src/app/shared/guards/newsletter-access.guard.ts
// `_notice: 'access'` mirrors writerGuard's denial convention — AppComponent turns it
// into the generic "Access Denied" toast (survives the SSR redirect, unlike a
// guard-side MessageService.add, which has no DOM on the server).
return router.createUrlTree([overviewPath], { queryParams: { project: slug, _notice: 'access' } });

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.

Non-blocking: _notice=access also changes list/create denials from silent redirects to toasts, contrary to #1570’s unchanged-behavior criterion. Either retain the legacy redirect here or document acceptance of this UX change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documenting acceptance of this UX change: legacy-chain denials (list/create) now surface the same "Access Denied" toast writerGuard uses app-wide via _notice: 'access', instead of redirecting silently — a silent redirect read as a broken link. Also corrected the PR body, which had claimed the legacy chain was unchanged, and added a Behavior changes row for it.

Comment thread apps/lfx-one/src/app/shared/utils/entity-project-context.util.ts
Signed-off-by: Audi Young <audi.mycloud@gmail.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b05b4b9. Configure here.

return;
}
// Mirror syncEntityProjectContext: only write ?project= to the URL when already present.
const syncUrl = 'project' in urlParams;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale project query never repaired

Medium Severity

The new urlAgrees fall-through still cannot repair a stale ?project= param. Both the activation apply and the NavigationEnd re-apply call setProject/setFoundation while router.getCurrentNavigation() is still set, so syncProjectQueryParam no-ops. In Angular 20 the current navigation is cleared only after NavigationEnd subscribers finish, so the old slug remains for the session.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b05b4b9. Configure here.

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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment on lines +115 to +121
// null only ever comes from the catchError above — evict failures so the next
// caller retries instead of replaying a transient error for the rest of the
// session (GH-1570: a poisoned entry would pin the newsletter guard and route
// reconciliation to a stale context). Concurrent subscribers still share this
// emission; only future lookups re-fetch.
if (!project) {
this.projectCache.delete(cacheKey);
Comment on lines +210 to +212
// Mirror syncEntityProjectContext: only write ?project= to the URL when already present.
const syncUrl = 'project' in urlParams;
applyEntityProjectContext(projectContextService, resolved.context, resolved.isFoundation, syncUrl);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Newsletter edit and analytics pages can authorize against and display the wrong project

3 participants