Skip to content

ARCH-001 Phase 16: News controller/service consolidation (Admin/Store) + Comments tab fix - #810

Merged
KrzysztofPajak merged 10 commits into
developfrom
arch001/phase16-news-consolidation
Sep 3, 2026
Merged

ARCH-001 Phase 16: News controller/service consolidation (Admin/Store) + Comments tab fix#810
KrzysztofPajak merged 10 commits into
developfrom
arch001/phase16-news-consolidation

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Resolves ARCH-001 Phase 16
Type: bugfix, refactor

Issue

Grand.Web.Admin and Grand.Web.Store each carried their own NewsController (228 / 234 lines, no
Vendor equivalent) duplicating CRUD, comment-moderation, and access-control logic for NewsItem instead
of sharing it through Grand.Web.AdminShared. Same class of drag and security-gap risk as every prior
ARCH-001 phase — but investigating this entity's shape surfaced something new: a live, pre-existing
bug independent of the duplication itself.
Store's Edit.cshtml already ships a fully-built "Comments"
tab (identical Kendo grid markup to Admin's) whose AJAX calls target /Store/News/Comments and
/Store/News/CommentDelete — but Store's NewsController has never had either action. The tab has
always been broken for every Store user.

Solution

Consolidated both hosts behind a shared BaseNewsController in Grand.Web.AdminShared, driven by a new
RoutedNewsItemDataScope (IAdminDataScope<NewsItem>). No Vendor work needed.

  • BaseNewsController holds List/Create/Edit/Delete and — the headline change — Comments/
    CommentDelete, now genuinely shared rather than left Admin-only. This makes Store's already-built tab
    work for the first time.
  • Both concrete NewsControllers are thin subclasses. Store also keeps EditWarningCheck (the same
    proven re-derived condition already used by Category/Blog/Page) and its own Preview action.
  • 7 views moved to Grand.Web.AdminShared/Views/AdminShared/News/... — unlike Page's phase,
    List.cshtml unifies too (News's list is a single shared grid, not a two-tab split). 10 widget zones
    extracted into real per-host satellite partials (Store's <vc:admin-widget> calls were dead literal
    HTML before this change, the same bug class found and fixed in every prior phase).

Full design rationale: docs/superpowers/specs/2026-09-04-arch001-news-consolidation-design.md (spec)
and docs/superpowers/plans/2026-09-04-arch001-news-consolidation.md (plan) — both kept on disk only
(gitignored), summarized here.

Disclosed behavior changes (intentional, not regressions)

  • Comments/CommentDelete now require IAdminDataScope access checks that Admin's originals never
    had
    (CommentDelete uses HasAccess, Comments uses CanView) — required now that these actions
    are reachable from Store; Admin is unaffected since its scope is a no-op.
  • List(POST) now uses the shared INewsViewModelService.PrepareNewsItemModel for both hosts,
    dropping Store's original manual re-implementation, which also loaded NewsSettings via
    ISettingService.LoadSetting<NewsSettings> without ever using the result (confirmed dead code). As a
    side effect, Store's news list now shows unpublished, scheduled, expired, and customer-group-
    restricted items
    it couldn't see before (matching Admin's existing behavior) — Store managers
    couldn't previously see their own drafts on this screen. No tenant-isolation impact: store filtering
    and the SearchStoreId forcing are unchanged.
  • Create(POST) no longer double-writes. Store's original called _newsService.UpdateNews(newsItem)
    immediately after InsertNewsItemModel (which already inserts internally) — a genuine, purposeless
    redundant write, removed.
  • A pre-existing missing-await bug in Create(GET) and its invalid-redisplay path is fixed
    ViewBag.AllLanguages was being assigned an unawaited Task object instead of the resolved language
    list (Admin's own Edit actions already did this correctly).
  • Store's dead SearchNewsTitle search-by-title JS plumbing was dropped, not fixed into a working
    feature.
    Both hosts' original code was already non-functional here (Store hardcoded an empty string;
    Admin never sent the field at all) — implementing a real search-by-title feature would have been scope
    creep beyond fixing what's broken.
  • Store's leftover #search-newsitem click handler was correctly kept, deviating from the plan's own
    instruction to drop it — the unified List.cshtml still renders that button for Admin, so removing the
    handler would have broken Admin's live search. A real implementer catch of a plan defect.

Final whole-branch review

Dispatched on the most capable model per this initiative's process. Verdict: "Ready to merge: With
fixes"
— 1 Critical, found by re-deriving tenant isolation from the two original controllers rather
than trusting that per-task reviews already covered it:

Critical, fixed: Comments(POST) had no access check at allCommentDelete got the "add the
protection Store now needs, even though Admin's original never needed one" treatment, but its sibling
read action, fifteen lines above it in the same task's diff, did not (because that code was a verbatim,
untouched copy of Admin's already-correct original, which never needed a check since Admin is global).
Two real leaks existed before the fix: a store manager could pass a different store's news item id and
read its full comment list — including commenter email addresses — and an empty id fell through to
GetAllComments(""), returning every comment across every store in the installation. Fixed with a
CanView gate (deliberately not the stricter HasAccess, so the tab still opens for global/multi-store
items a Store user is already permitted to view via Edit); the empty-id leak closes as a side effect
since GetNewsById("") returns null. Scoped re-review confirmed the fix, no new breakage.

Breaking changes

None for storefront/API consumers. See "Disclosed behavior changes" above — the news-list visibility
widening for Store managers is the most user-visible change, and it's a widening, not a leak.

Testing

Automated (all green):

  • dotnet build GrandNode.sln — 0 errors.
  • dotnet test src/Tests/Grand.Web.Admin.Tests — full suite green (1025+ tests, incl. tests proving each
    of the 3 ruled fixes and the Comments/CommentDelete access gates).
  • dotnet test src/Tests/Grand.Web.Store.Tests — full suite green (67 tests).
  • dotnet test src/Tests/Grand.Mapping.Tests — 234/234.

Live smoke test (run directly against a real dev DB, not sandboxed — specifically targeting this phase's
headline fix):

  1. Kestrel instance on a throwaway port, real MongoDB dev database.
  2. As Admin: List renders correctly with the store-filter dropdown; created a news item limited
    exclusively to a second store ("Store2"); confirmed its Edit screen's Comments tab loads a real,
    working Kendo grid (not a 404/error) — positive control on the fix's foundation.
  3. As a different store's user ("store1"): Edit(GET) denied (redirect to List), and the Store2-only
    item correctly excluded from store1's own news list. Crafted antiforgery-tokened requests to
    /Store/News/Comments with the Store2-only item's id, and again with an empty id, both denied
    with "No access to this news item's comments" — directly proving both halves of the Critical fix.
    Delete denied, redirected to List.
  4. Positive control as the owning store's user ("store2"): Edit succeeded with correct field
    visibility (no CustomerGroups/Stores fields); clicked the Comments tab in the actual browser UI and
    confirmed the Kendo grid genuinely initialized and loaded (total: 0, not an error) — this is the
    concrete, end-to-end proof that Store's Comments tab, broken since before this initiative existed, now
    works.
  5. Zero 404s on either host — the recurring [Area]/[Authorize*] omission that has hit roughly half of
    this initiative's phases did not recur here.
  6. All synthetic test data deleted afterward; DB confirmed restored to its original 3-item state.

Disclosed, out-of-scope finding: navigating directly to the bare GET /Admin/News/Comments action
(not the tab — the standalone route) throws a 500, because no Comments.cshtml view has ever existed for
this action on either host. Confirmed via the pre-consolidation commit that List.cshtml's "View
comments" link has always pointed at Edit/{id}, never at this action — it's dead, pre-existing code
from before this initiative, untouched by this PR.

🤖 Generated with Claude Code

KrzysztofPajak and others added 6 commits September 3, 2026 11:45
…+ 3 ruled bug fixes

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tore's broken Comments tab)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…regression tests

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…st.cshtml, extract widget-zone satellites

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ents(POST), closing a cross-tenant comment-read leak on Store

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 3, 2026 11:41

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.

🟡 Changes recommended

A newly exposed Comments(GET) endpoint returns a non-existent view (runtime 500) and should be made to fail gracefully before merge.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Consolidates duplicated Admin and Store News CRUD/comment-moderation logic behind a shared BaseNewsController in Grand.Web.AdminShared, introducing a routed per-request IAdminDataScope<NewsItem> to preserve tenant isolation, and extracting host-specific widget zones into satellite partials so the shared views can remain host-agnostic. This also enables Store’s previously-shipped-but-broken “Comments” tab by adding the missing Store-reachable Comments / CommentDelete endpoints (with proper access checks).

Changes:

  • Introduced BaseNewsController and RoutedNewsItemDataScope to centralize News actions while enforcing correct per-area access scope.
  • Moved/unified News views into Grand.Web.AdminShared and extracted widget-zone hooks into per-host partials (Admin vs Store).
  • Added characterization/unit tests covering scope routing, host attributes, and the merged access-check behavior.
File summaries
File Description
src/Web/Grand.Web.Store/Controllers/NewsController.cs Converted Store News controller to a thin BaseNewsController subclass; keeps Store-only warning hook + Preview.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.ListButtons.cshtml Store widget-zone hook for list buttons.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.DetailsTabs.cshtml Store widget-zone hook for details tab extensions.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.DetailsSeoTop.cshtml Store widget-zone hook for SEO top content.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.DetailsSeoBottom.cshtml Store widget-zone hook for SEO bottom content.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.DetailsInfoTop.cshtml Store widget-zone hook for info top content.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.DetailsInfoBottom.cshtml Store widget-zone hook for info bottom content.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.DetailsCommentsTop.cshtml Store widget-zone hook for comments top content.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.DetailsCommentsList.cshtml Store widget-zone hook for comments list content.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.DetailsCommentsBottom.cshtml Store widget-zone hook for comments bottom content.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.DetailsButtons.Edit.cshtml Store widget-zone hook for edit-page buttons.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/WidgetZone.DetailsButtons.Create.cshtml Store widget-zone hook for create-page buttons.
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/CreateOrUpdate.TabInfo.cshtml Removed Store-specific News “Info” tab partial (now provided by shared views).
src/Web/Grand.Web.Store/Areas/Store/Views/News/Partials/CreateOrUpdate.cshtml Removed Store-specific News tabstrip partial (now provided by shared views).
src/Web/Grand.Web.Store/Areas/Store/Views/News/List.cshtml Removed Store-specific News list view (now unified/shared).
src/Web/Grand.Web.Store/Areas/Store/Views/News/Edit.cshtml Removed Store-specific edit view (now shared).
src/Web/Grand.Web.Store/Areas/Store/Views/News/Create.cshtml Removed Store-specific create view (now shared).
src/Web/Grand.Web.AdminShared/Views/AdminShared/News/Partials/CreateOrUpdate.TabSeo.cshtml Updated shared SEO tab to delegate widget zones via host-specific partials.
src/Web/Grand.Web.AdminShared/Views/AdminShared/News/Partials/CreateOrUpdate.TabInfo.cshtml Updated shared Info tab; conditionally renders Admin-only fields and delegates widget zones via partials.
src/Web/Grand.Web.AdminShared/Views/AdminShared/News/Partials/CreateOrUpdate.TabComments.cshtml Updated shared Comments tab; routes AJAX endpoints by current area and delegates widget zones via partials.
src/Web/Grand.Web.AdminShared/Views/AdminShared/News/Partials/CreateOrUpdate.cshtml Updated shared tabstrip to delegate widget zones via host-specific partials.
src/Web/Grand.Web.AdminShared/Views/AdminShared/News/List.cshtml Unified List view for Admin/Store; uses route area and conditionally shows Admin-only store selector UI.
src/Web/Grand.Web.AdminShared/Views/AdminShared/News/Edit.cshtml Shared Edit view; binds asp-area to current route and delegates button widget zone via partial.
src/Web/Grand.Web.AdminShared/Views/AdminShared/News/Create.cshtml Shared Create view; binds asp-area to current route and delegates button widget zone via partial.
src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs Registers routed IAdminDataScope<NewsItem> for correct per-area resolution in combined host.
src/Web/Grand.Web.AdminShared/Services/RoutedNewsItemDataScope.cs New: routes NewsItem data scope by request area, failing closed on missing/unrecognized areas.
src/Web/Grand.Web.AdminShared/Controllers/BaseNewsController.cs New: shared controller implementing CRUD + comment moderation with scope gates and shared view usage.
src/Web/Grand.Web.Admin/Controllers/NewsController.cs Converted Admin News controller to a thin BaseNewsController subclass with required Admin host attributes.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.ListButtons.cshtml Admin widget-zone hook for list buttons.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.DetailsTabs.cshtml Admin widget-zone hook for details tab extensions.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.DetailsSeoTop.cshtml Admin widget-zone hook for SEO top content.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.DetailsSeoBottom.cshtml Admin widget-zone hook for SEO bottom content.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.DetailsInfoTop.cshtml Admin widget-zone hook for info top content.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.DetailsInfoBottom.cshtml Admin widget-zone hook for info bottom content.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.DetailsCommentsTop.cshtml Admin widget-zone hook for comments top content.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.DetailsCommentsList.cshtml Admin widget-zone hook for comments list content.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.DetailsCommentsBottom.cshtml Admin widget-zone hook for comments bottom content.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.DetailsButtons.Edit.cshtml Admin widget-zone hook for edit-page buttons.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/WidgetZone.DetailsButtons.Create.cshtml Admin widget-zone hook for create-page buttons.
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/CreateOrUpdate.TabSeo.cshtml Removed Admin-specific SEO tab partial (now shared).
src/Web/Grand.Web.Admin/Areas/Admin/Views/News/Partials/CreateOrUpdate.TabComments.cshtml Removed Admin-specific Comments tab partial (now shared).
src/Tests/Grand.Web.Store.Tests/Controllers/NewsControllerTests.cs New: Store controller attribute + warning-hook + preview behavior tests.
src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedNewsItemDataScopeTests.cs New: validates routed scope resolves correctly and fails closed.
src/Tests/Grand.Web.Admin.Tests/Controllers/NewsControllerTests.cs New: validates Admin controller is a thin subclass and carries required attributes.
src/Tests/Grand.Web.Admin.Tests/Controllers/BaseNewsControllerTests.cs New: characterization tests for shared controller behavior (list forcing, language loading, scope gates, comment moderation).
Review details
  • Files reviewed: 45/45 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Web/Grand.Web.AdminShared/Controllers/BaseNewsController.cs Outdated
Comment thread src/Web/Grand.Web.Store/Controllers/NewsController.cs Fixed

var view = result as ViewResult;
Assert.IsNotNull(view);
var model = (NewsItemListModel)view.Model;

var view = result as ViewResult;
Assert.IsNotNull(view);
var model = (NewsItemListModel)view.Model;

var redirect = result as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("List", redirect.ActionName);

var redirect = result as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("List", redirect.ActionName);

var redirect = result as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("Edit", redirect.ActionName);

var redirect = result as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("List", redirect.ActionName);

var redirect = result as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("List", redirect.ActionName);

var json = result as JsonResult;
Assert.IsNotNull(json);
var gridModel = (DataSourceResult)json.Value;

var json = result as JsonResult;
Assert.IsNotNull(json);
var gridModel = (DataSourceResult)json.Value;
…er.EditWarningCheck

Same CodeQL finding/fix as Page (arch001/phase15): the inner
'newsItem.LimitedToStores &&' re-check in the OR's second branch is
always true and was dead code inherited from the Category/Blog idiom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
KrzysztofPajak and others added 3 commits September 3, 2026 21:10
…ing nonexistent view

Copilot review on PR #810: BaseNewsController.Comments(string) returned View()
but no Comments.cshtml exists for News (unlike Blog, which has one). Preexisting
bug carried over from Admin's original NewsController, verified against the
merge-base. The GET action is unreachable via the UI — the Comments tab's grid
calls the POST overload only — so redirect to List instead of leaving a dead
View() call that 500s if ever hit directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
Comments_ReturnsView asserted the old (buggy) View() behavior. Replaced with
Comments_RedirectsToList to match the redirect fix in the prior commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo

var redirect = result as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("List", redirect.ActionName);
@KrzysztofPajak
KrzysztofPajak merged commit 92bce57 into develop Sep 3, 2026
6 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the arch001/phase16-news-consolidation branch September 3, 2026 20:05
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.

2 participants