Store-manager customer management + per-store customer identity#717
Open
KrzysztofPajak wants to merge 17 commits into
Open
Store-manager customer management + per-store customer identity#717KrzysztofPajak wants to merge 17 commits into
KrzysztofPajak wants to merge 17 commits into
Conversation
Workstream A - per-store customer identity (appsettings-driven): - New CustomerConfig.RegisterCustomersPerStore flag (bound from the "Customer" section; documented in all host appsettings.json). Default false keeps behaviour unchanged. - ICustomerService.GetCustomerByEmail/GetCustomerByUsername gain an optional storeId (empty => global). Wired through registration, storefront login/2FA, password recovery, account activation, profile/sub-account editors and the AdminShared CustomerValidator (scoped only when the flag is on). - CookieAuthenticationService now writes a customer-id claim and re-resolves the session via GetCustomerById (unambiguous with per-store), with a fallback for pre-existing cookies. ChangePassword callers holding a customer pass StoreId. - Compound Email+StoreId / Username+StoreId indexes added (non-unique; uniqueness enforced in the app layer). Workstream B - store-manager customer panel (Grand.Web.Store): - New CustomerController managing only the Registered customers of the manager's store, reusing AdminShared ICustomerViewModelService/CustomerModel. Ownership guard + server-forced StoreId/Registered group/cleared Owner/Vendor/Staff/Se. - Store-area views adapted from Admin; TabInfo strips role/Owner/Vendor/Staff/ Store fields; tabs shown except Documents, Notes and ActivityLog. No export, no impersonation. - ManageCustomers added to StoreManager default permissions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Issued storefront-API tokens now carry a CustomerId claim and authentication resolves the customer by GetCustomerById (unambiguous with per-store identity), falling back to e-mail/guid for tokens issued before the claim existed: - TokenWebController.Login resolves email+store and adds the CustomerId claim; Refresh rebuilds the claims from the resolved customer. - ApiAuthenticationService (JWT + frontend scheme) and JwtBearerCustomerAuthenticationService prefer the CustomerId claim. - LoginWebValidator scopes the credential check to the current store. Backend/admin API and back-office panel login stay global by design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The store-manager customer panel only makes sense with per-store customer identity. When Customer:RegisterCustomersPerStore is off, a controller gate routes every action to a dedicated PerStoreDisabled action/view explaining how to enable the setting (no ViewBag). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CustomerService: store-scoped GetCustomerByEmail/GetCustomerByUsername (matching store, no-match returns null). - CustomerManagerService: LoginCustomer/ChangePassword thread the storeId into the lookup. - Store CustomerController: per-store gate (redirect to PerStoreDisabled when off, allowed when on / for the notice action) and Create forcing store-scoped, registered-only constraints regardless of posted values. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The customer auth cookie is host-only, so a different subdomain already gets a separate session. As defense in depth (covers shared parent-domain cookies or several stores on one host), WorkContextSetter now rejects a cookie-authenticated storefront shopper whose StoreId differs from the current store when Customer:RegisterCustomersPerStore is on. Back-office accounts (admin/store manager/sales/vendor) are excluded, so panel login is unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shipped default must stay off (consistent with the docs and the other hosts); the flag was left on from local testing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WorkContextSetter store-isolation guard (and IsStorefrontShopper) is removed: the customer auth cookie is host-only, so a different subdomain already gets a separate session. The guard only helped atypical deployments (shared parent-domain cookie / several stores on one host) at the cost of back-office role exclusions. Impersonation is unchanged (admin-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With per-store identity on, a store customer could reuse the admin's e-mail (admin has no StoreId), and the global GetCustomerByEmail/Username used by back-office panel login could resolve the store customer instead of the admin, locking the admin out. Global (empty-storeId) lookups now prefer the store-independent account (StoreId null/"") and fall back to any match, only when the flag is on. CustomerService takes CustomerConfig. Added a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- GetCustomerByEmail/Username: a store-scoped lookup (storefront login) now falls back to the store-independent system/back-office account (e.g. the storeless administrator) when the store has no matching customer, so the admin can sign in through the storefront under per-store identity. - HeaderLinks: show the Store portal link only when StaffStoreId is non-empty (it can be "" after a customer is saved via the editor), not just non-null. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tity
The backend customer API addresses customers by e-mail, which is not unique once
per-store identity is on - and the global lookup prefers the store-independent
account. DeleteCustomer only blocks built-in system accounts, not the admin, so a
DELETE /Customer/{email} meant for a store customer could resolve and delete the
administrator. Refuse to delete a store-independent (StoreId empty) account through
this by-email endpoint while per-store identity is enabled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ore identity PrepareMerchandiseReturnModel resolved the search customer by e-mail globally; with per-store identity the same e-mail can exist in several stores. Scope the lookup to the current store (empty when the flag is off) so the search matches this store's customer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers: empty/null/whitespace email, case-insensitivity, store-scoped match, store-scoped fallback to the storeless account (StoreId "" and null), store wins over storeless, no-match-no-storeless returns null, global prefer-storeless, global fallback to any match, and the per-store-off branches (exact store match, no fallback, global match). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| { | ||
| var customAttributes = | ||
| await model.Address.ParseCustomAddressAttributes(_addressAttributeParser, _addressAttributeService); | ||
| address = await _customerViewModelService.UpdateAddressModel(customer, address, model, customAttributes); |
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
- JwtBearerCustomerAuthenticationService / CookieAuthenticationService: resolve the authenticated customer by the CustomerId claim (GetCustomerById) instead of e-mail. - PermissionProvider: StoreManager default permissions include ManageCustomers. Co-Authored-By: Claude Opus 4.8 <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.


Summary
Adds customer management for store managers in
Grand.Web.Storeand an opt-in per-store customer identity mode (e-mail/username unique per store instead of globally), configured fromappsettings.json.Both features are off by default (
Customer:RegisterCustomersPerStore = false) → no behavioural change unless enabled.Workstream A — Per-store customer identity (appsettings)
CustomerConfig.RegisterCustomersPerStore("Customer"section, bound inStartupBase, documented in all host appsettings). When on, the uniqueness key becomes e-mail/username + StoreId; customers stay in one collection (discriminated byStoreId).ICustomerService.GetCustomerByEmail/GetCustomerByUsernamegained an optionalstoreId:CustomerManagerService.LoginCustomer/ChangePassword, profile & sub-account validators, AdminSharedCustomerValidator, and the Frontend API (tokens carry aCustomerIdclaim; auth resolves byGetCustomerById, with e-mail/guid fallback for old tokens).Email_StoreId/Username_StoreIdindexes added.BaseLoginController) and backend/admin API stay global by design.DELETE /Customer/{email}refuses to delete a store-independent (admin/back-office) account under per-store; storefront Store-portal link now shown only to real staff (StaffStoreIdnon-empty); vendor merchandise-return search scoped to the current store.Workstream B — Store-manager customer panel (
Grand.Web.Store)CustomerControllermanaging only the Registered customers of the manager's store, reusing the AdminSharedICustomerViewModelService/CustomerModel. Ownership guard + server-forcedStoreId/Registered group/cleared Owner/Vendor/Staff/Se.PerStoreDisabledpage.ManageCustomersadded to StoreManager default permissions.Tests
CustomerService: full branch coverage for the store-aware/storeless lookups.CustomerManagerService: storeId threading for login/change-password.Grand.Web.StoreCustomerController: per-store gate + forced store/registered-only constraints.Notes / known caveat
🤖 Generated with Claude Code