ARCH-001 Phase 1 (WIP, Tasks 1-9/14): consolidate Admin/Store/Vendor ProductController + ProductViewModelService - #790
Draft
KrzysztofPajak wants to merge 144 commits into
Draft
Conversation
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>
…(ARCH-001 Phase 2)
`<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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #issueNumber
Type: feature
Issue
Grand.Web.Admin,Grand.Web.Store, andGrand.Web.Vendoreach ship their own ~2500-lineProductControllerplus (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.mdFull plan (this PR implements Tasks 1-9 of 14):
docs/superpowers/plans/2026-08-16-arch001-product-consolidation-phase1.mdSolution
This is a draft / work in progress covering Phase 1's controller+service consolidation, Tasks 1-9 of the 14-task plan:
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.GlobalAdminDataScope<TEntity>(Admin — no-op/always-true),StoreAdminDataScope<TEntity>(Store — delegates to the existingAclMappingExtension.AccessToEntityByStore),VendorProductDataScope(Vendor —VendorId == CurrentVendor.Id).BaseProductControllerinGrand.Web.AdminSharednow 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) hadstoreIdparameters dropped from 13 methods; the service now resolves scope internally via the injectedIAdminDataScope<Product>(Task 9).ProductReservationDelete/BidDeletewhere 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-breakingContent()denial response).Not yet in this PR (remaining Tasks 10-14, tracked in the plan):
ProductViewModelServicemethods.ProductControllers into thinBaseProductControllersubclasses (blocked on Task 10'sPrepareBulkEditProductModelvendor-scoping prerequisite).ProductViewModelService, finish DI cutover..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-hostsBaseProductController), except for Task 9'sIProductViewModelServicesignature change, which required updating call sites in Store's still-activeProductController.csto 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 astoreIdparameter) — this is an internalGrand.Web.AdminSharedcontract, not exposed outside the three admin-panel hosts, and both existing consumers (Store's controller, and this PR's ownBaseProductController) were updated in the same commit.Testing
dotnet buildonGrand.Web.AdminShared,Grand.Web.Admin,Grand.Web.Store,Grand.Web.Vendor— all succeed, 0 errors.dotnet test src/Tests/Grand.Web.Admin.Tests(unfiltered) — 395 passed, 0 failed.dotnet test src/Tests/Grand.Web.Store.Tests(unfiltered) — 122 passed, 0 failed.dotnet test src/Tests/Grand.Web.Vendor.Tests(unfiltered) — 19 passed, 0 failed.