Skip to content

ARCH-001 Phase 1 (WIP, Tasks 1-9/14): consolidate Admin/Store/Vendor ProductController + ProductViewModelService - #790

Draft
KrzysztofPajak wants to merge 144 commits into
developfrom
arch001/phase1-product-consolidation
Draft

ARCH-001 Phase 1 (WIP, Tasks 1-9/14): consolidate Admin/Store/Vendor ProductController + ProductViewModelService#790
KrzysztofPajak wants to merge 144 commits into
developfrom
arch001/phase1-product-consolidation

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Resolves #issueNumber
Type: feature

Issue

Grand.Web.Admin, Grand.Web.Store, and Grand.Web.Vendor each ship their own ~2500-line ProductController plus (Admin/Store share one, Vendor has its own) ProductViewModelService, duplicating product CRUD, associated/related/similar/bundle/cross-sell/recommended product mapping, tier prices, attributes, combinations, reservations, bids, and every access check three times over. The three copies have already drifted: several confused-deputy IDORs (access checked on one caller-supplied id, mutation performed on a different unverified one) and multiple "no check at all" gaps existed only in one or two of the three controllers, undetected because there's no shared surface to review or test once.

Full design: docs/superpowers/specs/2026-08-16-arch001-product-consolidation-design.md
Full plan (this PR implements Tasks 1-9 of 14): docs/superpowers/plans/2026-08-16-arch001-product-consolidation-phase1.md

Solution

This is a draft / work in progress covering Phase 1's controller+service consolidation, Tasks 1-9 of the 14-task plan:

  • New IAdminDataScope<TEntity> strategy interface (Grand.Web.AdminShared) abstracting per-host access rules: HasAccess (strict/mutation check), CanView (default = HasAccess, looser Store-only override for read/reference actions), ApplyScope, DefaultStoreId, ResourceKeyPrefix, ShowStoreSelector.
  • Three implementations registered per host's DI container: GlobalAdminDataScope<TEntity> (Admin — no-op/always-true), StoreAdminDataScope<TEntity> (Store — delegates to the existing AclMappingExtension.AccessToEntityByStore), VendorProductDataScope (Vendor — VendorId == CurrentVendor.Id).
  • BaseProductController in Grand.Web.AdminShared now contains all 24 regions migrated from the three original controllers, verified to be a strict superset of their combined public action surface (Task 8's Step 3 diff check).
  • IProductViewModelService/ProductViewModelService (Grand.Web.AdminShared) had storeId parameters dropped from 13 methods; the service now resolves scope internally via the injected IAdminDataScope<Product> (Task 9).
  • Along the way: ~20 real IDORs closed (mostly mapping-table-type ownership gaps missing on one or two hosts; two confused-deputy-class bugs in ProductReservationDelete/BidDelete where the checked id and the mutated id were different, unverified caller-supplied fields), plus several pre-existing UI/UX bugs (a silent Kendo-grid message-drop, a JSON-parse-breaking Content() denial response).

Not yet in this PR (remaining Tasks 10-14, tracked in the plan):

  • Task 10: reconcile the ~29 remaining ProductViewModelService methods.
  • Task 11: convert the three host ProductControllers into thin BaseProductController subclasses (blocked on Task 10's PrepareBulkEditProductModel vendor-scoping prerequisite).
  • Task 12: delete Vendor's now-duplicate ProductViewModelService, finish DI cutover.
  • Task 13: consolidate/trim characterization tests.
  • Task 14: full-solution verification.
  • Phase 2 (separate plan/PR): view-layer (.cshtml) consolidation.

Until Task 11 lands, the three original host ProductControllers remain the live code path in production — this PR is purely additive (new shared types + a still-unused-by-hosts BaseProductController), except for Task 9's IProductViewModelService signature change, which required updating call sites in Store's still-active ProductController.cs to keep it compiling (mechanical parameter-drop only, no behavior change).

Breaking changes

None for end users. IProductViewModelService's public method signatures changed (13 methods lost a storeId parameter) — this is an internal Grand.Web.AdminShared contract, not exposed outside the three admin-panel hosts, and both existing consumers (Store's controller, and this PR's own BaseProductController) were updated in the same commit.

Testing

  1. dotnet build on Grand.Web.AdminShared, Grand.Web.Admin, Grand.Web.Store, Grand.Web.Vendor — all succeed, 0 errors.
  2. dotnet test src/Tests/Grand.Web.Admin.Tests (unfiltered) — 395 passed, 0 failed.
  3. dotnet test src/Tests/Grand.Web.Store.Tests (unfiltered) — 122 passed, 0 failed.
  4. dotnet test src/Tests/Grand.Web.Vendor.Tests (unfiltered) — 19 passed, 0 failed.
  5. No manual/UI testing yet — the three original controllers are still what's actually routed to until Task 11, so there is nothing new to click through in this PR.

KrzysztofPajak and others added 30 commits August 16, 2026 14:20
Two-phase design: Phase 1 consolidates ProductController and
ProductViewModelService into Grand.Web.AdminShared via a new
IAdminDataScope<TEntity> abstraction, following the existing
BaseLoginController precedent. Phase 2 consolidates the Product
views via an extended ViewLocationExpander. Scoped to the Product
vertical only; other entities and a full panel merge are out of
scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e consolidation)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s for dropped storeId params

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ic init lives in PaymentControllerTests)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…CH-001 Phase 1)

Task 7 of ARCH-001 Phase 1: creates the abstract BaseProductController in
Grand.Web.AdminShared, migrating only the first region ("Product list /
create / edit / delete") as a worked template for Task 8's remaining 23
regions. Consumes IAdminDataScope<Product> (Tasks 1-4) to unify Admin's
no-op access check, Store's AccessToEntityByStore check, and Vendor's
HasAccessToProduct check into a single scope.HasAccess(product) call.

Deliberate behavior change: GoToSku's access-denial path now redirects to
List instead of Edit. Store's pre-refactor GoToSku had a bug where an
access-denied SKU lookup fell through to redirect to Edit (bypassing the
intended denial) while an access-granted lookup incorrectly showed a
"not found" warning. The merged behavior fixes both: denied -> List,
granted -> Edit. See the TODO(ARCH-001-followup) comment at the call site.

Resource key prefix header comment carries forward Task 6's corrected
28-suffix audit (22 templated via scope.ResourceKeyPrefix, 6 Admin-only
literals, 0 host-specific).

Not yet wired to any host controller - BaseProductController is abstract
and incomplete until Task 8 migrates the rest and Task 11 adds host
subclasses.
…line Task 6 table

Task 7's review found: (1) Critical - DeleteSelected shipped with no scope filter at
all, handing Store host an unscoped bulk-delete endpoint once Task 11 subclasses it;
(2) Important - HasAccess alone can't represent Store's actual Edit(GET)/CopyProduct
behavior, which is deliberately looser than the strict mutation rule (existing test
comment: 'the one path that must stay outside any shared authorize-or-redirect
helper') - added CanView (default interface method, additive) with a Store override
matching the original permissive rule; (3) Important - the resource-key table was
only referenced via an untracked planning file - inlined the full 28-row table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…re's looser view/copy rule (ARCH-001 Phase 1)

Applies the plan fix from commit dcddc85 (opus review of Task 7, confirmed
independently by the coordinator):

1. Critical - DeleteSelected shipped with zero scope enforcement. Once Task 11
   subclasses BaseProductController into the Store host, that was a brand-new
   unscoped bulk-delete endpoint (any store staff could delete any product id
   in the system via POST /Product/DeleteSelected). Fixed by loading the
   products via GetProductsByIds and filtering through scope.HasAccess (the
   strict mutation check, same one Edit(POST)/Delete already use) before
   delegating to the service.

2. Important - Edit(GET) and CopyProduct used the strict HasAccess, which is
   too strict for Store: the original Grand.Web.Store ProductController allows
   viewing/copying a global or multi-store product (including the staff
   member's store), only denying products limited to excluded stores. This is
   locked by an existing test with an explicit warning comment
   (Grand.Web.Store.Tests/Controllers/ProductControllerTests.cs:248-264,
   'the one path that must stay outside any shared authorize-or-redirect
   helper'). Added IAdminDataScope<TEntity>.CanView as a default interface
   method (additive, defaults to HasAccess - Global/Vendor scopes need no
   change) with a StoreAdminDataScope override implementing the looser rule.
   Edit(GET) and CopyProduct now gate on scope.CanView; Edit(POST)/Delete/
   DeleteSelected remain on the strict scope.HasAccess since those are
   mutations.

3. Important - the resource-key-prefix table lived only in the untracked
   .superpowers/ planning file. Inlined the full corrected table as the
   header comment in BaseProductController.cs so it survives in the repo.

New tests: StoreAdminDataScopeTests gets 3 CanView cases (9/9 total).
BaseProductControllerTests gets DeleteSelected filtering coverage and
CanView-vs-HasAccess regression guards for Edit(GET)/CopyProduct (21/21
total, up from 16).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…01 Phase 1)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…001 Phase 1)

Fix (post-review): template the Catalog.Products.Permissions denial message via
scope.ResourceKeyPrefix instead of hardcoding "Admin.". Task 6's audit only
scanned the files under migration and never saw a Vendor call site for this key,
but Vendor.Catalog.Products.Permissions genuinely exists at the XML resource
layer (en_220.xml, consumed by Grand.Web.Vendor's validators) - that audit's
scope was narrower than "Admin-only".
…1 Phase 1)

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

Merges RelatedProductList/Update/Delete/AddPopup(GET+POST)/AddPopupList
from Admin/Store/Vendor ProductController into BaseProductController,
resolved through IAdminDataScope<Product>.

- List/Update/Delete/AddPopup(POST) now gated by scope.HasAccess (strict),
  templated via scope.ResourceKeyPrefix. This closes a real IDOR: Vendor's
  original Update/Delete/AddPopup(POST) had zero ownership checks - only
  List was gated - letting any vendor mutate or attach related-product
  mappings on another vendor's product by id.
- AddPopup(GET) and AddPopupList keep the no-check / SearchStoreId-scoping
  behavior common to all three original hosts.
- PrepareRelatedProductModel called with scope.DefaultStoreId ?? "" per
  the established pattern (old storeId-parameter signature still in place;
  Task 9 removes it later).
- Added InvalidRelatedProductAddPopupResult protected virtual hook: Admin
  and Store both re-prepare + return View on invalid ModelState; Vendor
  instead returns Content(ModelState.GetErrors()), a Vendor-only extension
  AdminShared cannot reference. Default matches Admin/Store; documented for
  a future Vendor subclass override once hosts are subclassed (Task 11).

Tests: 18 new cases in BaseProductControllerTests covering granted/denied
scope checks for List/Update/Delete/AddPopup(POST), plus DefaultStoreId
plumbing for AddPopup(GET)/AddPopupList. 59/59 passing.
…1 Phase 1)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-001 Phase 1)

- Merges AssociatedProductList/Update/Delete/AddPopup(GET+POST)/AddPopupList
  from Admin/Store/Vendor ProductControllers into BaseProductController,
  gating all mutating actions through scope.HasAccess.
- Fixes a real gap: Vendor's controller had no ownership check on the parent
  product (model.ProductId) for AssociatedProductAddPopup(POST), in either
  layer. Vendor's own host-specific service (Grand.Web.Vendor/Services/
  ProductViewModelService.cs InsertAssociatedProductModel) already filtered
  each selected product via HasAccessToProduct before reparenting it, but
  the parent was never checked anywhere - letting a vendor attach their own
  products under another vendor's grouped product (cross-vendor storefront
  pollution / data-integrity issue, not a cross-tenant write to someone
  else's product record). Store's original already filtered selected ids at
  the controller level for the same reparenting reason (InsertAssociated-
  ProductModel mutates the selected products' own records, unlike Related/
  Similar/Bundle/Cross-sell/Recommended); that per-id filter is now applied
  uniformly at the controller level.
- This controller-level per-id filter is necessary regardless of that
  pre-existing severity: BaseProductController injects AdminShared's
  unfiltered IProductViewModelService, not Vendor's own filtered one. Once
  Task 11 subclasses Vendor onto BaseProductController, Vendor loses its
  service-layer filter entirely, so the controller must enforce both the
  parent and per-selected-id checks itself going forward.
- Admin's original AssociatedProductList/Update/Delete had no access check
  at all; now gated like Store/Vendor via scope.HasAccess.
- Added protected virtual AssociatedProductVendorId hook (default "") so
  Vendor's original vendor-filtered GetAssociatedProducts(vendorId:) call
  can be reinstated once hosts subclass BaseProductController (Task 11).
- No InvalidAssociatedProductAddPopupResult hook needed: unlike the other
  AddPopup(POST) actions in this file, all three original hosts share
  identical invalid-model-state handling here.
- Adds characterization/regression tests to BaseProductControllerTests.cs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…1 Phase 1)

Closes two Vendor-only IDOR gaps: ProductPicturePopup(POST) and
ProductPictureDelete had no ownership check at all, letting any vendor
mutate/delete another vendor's product picture by posting its
productId/model.Id. Admin never had access checks on any action in
this region either (GlobalAdminDataScope.HasAccess is a no-op), now
uniformly gated via scope.HasAccess.

Flagged, not fixed: ProductPictureAdd has no file-size limit in any of
the three original hosts (unlike the recently-hardened attribute
upload paths in commit a153496), buffering the full upload into
memory unconditionally once the extension check passes. Pre-existing
across all three hosts; ported as-is per this row's scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…troller (ARCH-001 Phase 1)

Real gap found: Vendor's ProductSpecAttrPopup(POST) and ProductSpecAttrDelete had no
access check at all, letting any vendor add/edit/delete specification attributes on
another vendor's product by posting its productId/model.Id. Merged onto scope.HasAccess,
matching Store's CanAccessProduct and closing the gap on Vendor without changing Admin's
superuser behaviour.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…H-001 Phase 1)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e under C# rules)

Vendor's future subclass needs a different IOrderViewModelService/OrderListModel
type entirely, which C#'s 'override' cannot express (parameter types must match
exactly). Removed the misleading 'virtual' modifier and corrected the comment to
describe the actual mechanism: Vendor's subclass will shadow this action with
'new' and its own types, not override it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… Phase 1)

- ExportExcelAll: identical in Admin/Vendor; relies on the host-specific
  IProductViewModelService.PrepareProducts implementation for scoping (Vendor's
  always filters by CurrentVendor.Id internally).
- ExportExcelSelected: applies scope.HasAccess per selected id unconditionally,
  matching Vendor's original explicit re-check and closing the same gap on Admin
  (which had none) for caller-supplied ids.
- ImportExcel: ported from Admin only, non-virtual. Vendor never grants the
  Products permission's Import action, so PermissionAuthorizeAction already
  gates it out for that host; no Store region existed for this at all.
- Flagged (not fixed, out of scope): ImportExcel has no file-extension allowlist
  or size cap before reading the stream into memory - same shape of gap commit
  a153496 fixed for attribute uploads, left untouched here as pre-existing
  behavior being ported verbatim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
KrzysztofPajak and others added 30 commits August 18, 2026 19:27
`<vc:admin-widget>` binds to AdminWidgetViewComponent in Grand.Web.Admin.
Razor binds tag helpers at compile time from the compiling project's
`@addTagHelper` set; Grand.Web.AdminShared only adds Grand.Web.Common, so
all 44 widget-zone calls in the moved Product views compiled to literal
`<vc:admin-widget .../>` markup — an Admin-host regression, silent at
build and at runtime.

Move the 40 `WidgetZone.*.cshtml` defaults into
Grand.Web.Admin/Areas/Admin/Views/Product/Partials/, where the tag helper
is registered and host-override precedence finds them first. Leave empty
commented placeholders in AdminShared so Store falls through to a
deliberate no-op instead of stray unbound markup (its pre-existing
behaviour, now explicit).

Extract the 4 inline calls in CreateOrUpdate.Discounts/.Documents into
WidgetZone.{Discounts,Documents}.{Top,Bottom} partial pairs following the
same pattern; those two parents stay in AdminShared since Store needs
them. Also closes M7: their hardcoded `Loc["Admin.…"]` keys now use
`Scope.ResourceKeyPrefix`.

Verified: literal `<vc:` count in Grand.Web.AdminShared.dll 44 -> 0;
AdminWidgetViewComponent refs in Grand.Web.Admin.dll 335 -> 379.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A Razor view's compiled path is a global lookup key across all
ApplicationParts. Grand.Web (the combined host) references Admin/Store/
Vendor and therefore transitively loads AdminShared's views into the same
path space it already occupies with its storefront views. Two confirmed
collisions: /Views/_ViewStart.cshtml (admin layout resolver vs the
storefront's `Layout = "_Layout"`) and
/Views/Product/Partials/ProductAttributes.cshtml (admin ProductModel
partial vs the storefront product-details partial). Either way one
silently shadows the other; the loser breaks at render time.

Move Views/Product/, _ViewImports.cshtml and _ViewStart.cshtml one level
down into Views/AdminShared/ and change
ViewLocationExpander.AdminSharedFallbackLocation to
/Views/AdminShared/{1}/{0}.cshtml. The _ViewStart ancestor walk still
resolves (/Views/AdminShared/Product/X.cshtml ->
/Views/AdminShared/_ViewStart.cshtml) and host-override precedence is
unchanged. No existing test asserts the literal fallback string.

This also removes the whole collision class for later phases: Order/,
Vendor/, Page/, Blog/, News/ and Catalog/ all already exist under
src/Web/Grand.Web/Views/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The `_ => globalScope` default arm covered "Admin", a missing HttpContext,
a missing area route value, and any future or mis-cased area - mapping all
of them to GlobalAdminDataScope, whose ApplyScope is a no-op and whose
HasAccess is always true. Unreachable today (all three hosts route via
MapAreaControllerRoute, so `area` is the route default and always present),
but fail-open is the wrong default on the one object enforcing store and
vendor tenant isolation.

"Admin" is now explicit and the fallback throws InvalidOperationException.
Adds RoutedProductDataScopeTests covering the routing decision itself
(Admin/Store/Vendor/unrecognized/missing area/no HttpContext), which the
three concrete scopes' own suites never exercised. Placed in
Grand.Web.Admin.Tests alongside GlobalAdminDataScopeTests, which already
tests an AdminShared service; no Grand.Web.AdminShared.Tests project exists.

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

_ViewImports gets a short comment block distinguishing
Scope.ResourceKeyPrefix (Admin-and-Store vs Vendor) from the "area" route
value (the only Admin-vs-Store discriminator), so later phases don't add a
third idiom.

_ViewStart no longer builds `~/Areas//Views/Shared/_Layout.cshtml` when the
area route value is absent - a null area now falls through instead of
throwing InvalidOperationException.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records that the /Views/{1}/{0}.cshtml fallback location (section 2) and
the AdminShared-hosted widget-zone default (section 4a) are superseded,
with the rules Phase 3 must follow instead, plus the fail-closed scope
resolver, the documented host-detection idioms, and the still-outstanding
manual smoke pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Section 6 addendum. Extends the widget-zone partial-override pattern
(section 4a/5.2) to the six remaining @if (Scope.ResourceKeyPrefix
!= "Vendor") sites in Product views plus one C# site in
ProductViewModelService, and adds a bool CanFeatureOnHomepage
capability flag to IAdminDataScope<TEntity> for the latter. Confirms
via a resource-file diff that Admin.*/Vendor.* catalog.products.*
values are identical, so ResourceKeyPrefix keeps its one declared job
(building Loc keys) without needing any resource-file changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements spec section 6. Replaces all 8 sites (6 originally audited
plus 2 found mid-implementation in CreateOrUpdate.Reviews.cshtml,
which used == instead of != for the same idiom) that branched on
Scope.ResourceKeyPrefix to hide or alter markup for Vendor:

- Whole-block presence/absence (Documents tab, UserFields tab,
  TierPrice Store/CustomerGroup fields, Additional's downloads
  section, Categories/Collections IsFeaturedProduct column) now uses
  the section 4a/5.2 partial-override mechanism: AdminShared holds
  the real content, Vendor's own view folder holds an empty override.
- Content-differs-not-disappears cases (Kendo grid template: link vs
  plain text, in Categories/Collections/Reviews) get the same
  treatment but both hosts get real content.
- The one C# site (ProductViewModelService's 'Show on homepage'
  filter option) gets a new bool CanFeatureOnHomepage on
  IAdminDataScope<TEntity>, true for Global/Store, false for Vendor -
  the fix the prior session's own comment on that line recommended.

ResourceKeyPrefix now does only its declared job (building Loc keys);
no view or service branches on it. Verified: dotnet build
GrandNode.sln clean; Grand.Web.AdminShared.dll still has zero literal
<vc: strings (section 5.2's regression guard); Admin/Store/Vendor
test suites green (418/33/9).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every other caller of PrepareProductsModel/PrepareProducts in
BaseProductController forces model.SearchStoreId = scope.DefaultStoreId
before calling, since storeId scoping (unlike vendorId) happens at the
call site, not inside the shared service method. ExportExcelAll was
the one omission - it kept a stale comment from before Phase 1's
consolidation claiming the service method was 'host-specific' and
self-scoping, which stopped being true once Vendor's separate
PrepareProducts implementation was deleted and unified into one
ProductViewModelService.

Impact: a Store-scoped user (Grand.Web.Store) with the Products/Export
permission could POST an arbitrary or blank SearchStoreId to
ExportExcelAll and export every store's products, not just their own.
ExportExcelSelected (the sibling action) was already safe - it
re-checks scope.HasAccess per product id.

Found via a user-requested audit of the AdminShared consolidation for
cross-store/cross-vendor data leaks. Added two regression tests
mirroring the existing *_UsesScopeDefaultStoreId /
*_NoDefaultStoreId_DoesNotOverrideModelSearchStoreId pattern already
used for the seven AddPopupList siblings. Grand.Web.Admin.Tests:
420/420 green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements spec section 7. Section 5.2 placed an empty placeholder at
all 44 Product WidgetZone.<Name>.cshtml files with the comment 'Store
has no equivalent widget zone' - checked against Store's original
pre-Phase-2 views, which had copy-pasted <vc:admin-widget> from Admin
without adapting it, and without Grand.Web.Admin ever being in Store's
own @addTagHelper set. That call was already dead literal markup in
Store's own pre-migration code (verified via git show on the
pre-migration commit) - the placeholder preserved a pre-existing bug,
not a real absence of capability.

Grand.Web.Store has its own StoreWidgetViewComponent (vc:store-widget),
actively used elsewhere (Home/Index, Home/Statistics, _StoreLayout)
with an established store_-prefixed zone-name convention. Generated
all 44 real WidgetZone.*.cshtml overrides under
Grand.Web.Store/Areas/Store/Views/Product/Partials/, mirroring
Vendor's vc:admin-widget -> vc:vendor-widget / product_X ->
vendor_product_X transform (vc:admin-widget -> vc:store-widget /
product_X -> store_product_X). Also fixed the same dead-widget
copy-paste in the three pre-existing Store-specific whole-file
overrides (CreateOrUpdate.{Info,Prices,PurchasedWithOrders}.cshtml).
Corrected the now-inaccurate 'Store has no equivalent' comment on all
44 AdminShared placeholders (now genuinely unreachable except as
Vendor's Discounts/Documents fallback).

New store_product_* zone names are not yet targeted by any widget
plugin, so this changes no visible behavior today - it makes the
extension point reachable, same as Vendor's equivalent zones were
when added.

Found via user-prompted 'Store powinien mieć vc:store-widget' check.
Verified: dotnet build GrandNode.sln clean; Grand.Web.AdminShared.dll
still 0 literal <vc: strings; Grand.Web.Store.dll gained 53
StoreWidgetViewComponent references (44 new + 9 pre-existing) in
place of what the copy-paste would otherwise have left as dead
literal markup; Admin/Store/Vendor tests green (420/33/9).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Flagged during the tenant-isolation audit: BaseProductController and
ProductViewModelService scope every read/write through the
storeId/vendorId parameters and HasAccess/CanView, never through an
IQueryable filter. ApplyScope's only callers were its own unit tests.

Removed the interface member, its four implementations
(GlobalAdminDataScope, StoreAdminDataScope, VendorProductDataScope,
RoutedProductDataScope's pass-through), and the two tests that only
existed to exercise it. Spec section 8.

Verified: dotnet build GrandNode.sln clean; Admin/Store/Vendor tests
419/33/8 (down 2 from the removed tests), all green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ocs/superpowers/

BaseProductController.cs: CodeQL flagged its [HttpPost] actions as
missing antiforgery validation. Investigated: no exploitable gap
exists at runtime - the class is abstract (never routable) and its
three concrete subclasses (Admin/Store/Vendor ProductController) each
already restate [AutoValidateAntiforgeryToken], which ASP.NET Core
resolves via the full type hierarchy of the concrete controller. This
is a static-analysis false positive (CodeQL doesn't follow the
attribute across the base/derived-class, cross-project boundary), but
the investigation surfaced a real fragility: protection depended on
every current and future host subclass remembering to restate the
attribute. Added [AutoValidateAntiforgeryToken] to the base class
itself - no runtime behavior change, removes the fragility, and gives
CodeQL something to see in the file it flagged. Spec section 9.

.gitignore: added docs/superpowers/ per request.

Verified: dotnet build GrandNode.sln clean; Admin/Store/Vendor tests
419/33/8 unchanged, all green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removes the 4 spec/plan files from git tracking - they stay on disk
locally, just no longer part of the repo history going forward.
Matches the .gitignore entry added earlier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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