diff --git a/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs b/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs index 9c06bc0d7e..e1d42be685 100644 --- a/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs +++ b/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs @@ -9,7 +9,6 @@ using Grand.Data; using Grand.Domain.Customers; using Grand.Domain.Stores; -using Grand.Infrastructure; using Grand.Infrastructure.Extensions; using Grand.SharedKernel.Extensions; using Grand.Mediator; @@ -31,7 +30,6 @@ public ExternalAuthenticationService( IGroupService groupService, IMediator mediator, IRepository externalAuthenticationRecordRepository, - IContextAccessor contextAccessor, IEnumerable externalAuthenticationProviders, CustomerSettings customerSettings, ExternalAuthenticationSettings externalAuthenticationSettings) @@ -44,7 +42,6 @@ public ExternalAuthenticationService( _groupService = groupService; _mediator = mediator; _externalAuthenticationRecordRepository = externalAuthenticationRecordRepository; - _contextAccessor = contextAccessor; _externalAuthenticationProviders = externalAuthenticationProviders; } @@ -58,7 +55,6 @@ public ExternalAuthenticationService( private readonly IGroupService _groupService; private readonly IMediator _mediator; private readonly IRepository _externalAuthenticationRecordRepository; - private readonly IContextAccessor _contextAccessor; private readonly IEnumerable _externalAuthenticationProviders; private readonly CustomerSettings _customerSettings; private readonly ExternalAuthenticationSettings _externalAuthenticationSettings; @@ -72,14 +68,16 @@ public ExternalAuthenticationService( /// /// Associated with passed external authentication parameters user /// Current logged-in user + /// Customer of the request (a guest when nobody is logged in) + /// Store identifier /// URL to which the user will return after authentication /// Result of an authentication protected virtual async Task AuthenticateExistingUser(Customer associatedUser, - Customer currentLoggedInUser, string returnUrl) + Customer currentLoggedInUser, Customer customer, string storeId, string returnUrl) { //log in guest user if (currentLoggedInUser == null) - return await LoginUser(associatedUser, returnUrl); + return await LoginUser(associatedUser, customer, storeId, returnUrl); //account is already assigned to another user if (currentLoggedInUser.Id != associatedUser.Id) @@ -95,10 +93,12 @@ protected virtual async Task AuthenticateExistingUser(Customer as /// /// Current logged-in user /// Authentication parameters received from external authentication method + /// Customer of the request (a guest when nobody is logged in) + /// Store identifier /// URL to which the user will return after authentication /// Result of an authentication protected virtual async Task AuthenticateNewUser(Customer currentLoggedInUser, - ExternalAuthParam parameters, string returnUrl) + ExternalAuthParam parameters, Customer customer, string storeId, string returnUrl) { //associate external account with logged-in user if (currentLoggedInUser != null) @@ -111,7 +111,7 @@ protected virtual async Task AuthenticateNewUser(Customer current //or try to register new user if (_customerSettings.UserRegistrationType != UserRegistrationType.Disabled) - return await RegisterNewUser(parameters, returnUrl); + return await RegisterNewUser(parameters, customer, storeId, returnUrl); //registration is disabled return Error(["Registration is disabled"]); @@ -121,33 +121,36 @@ protected virtual async Task AuthenticateNewUser(Customer current /// Register new user /// /// Authentication parameters received from external authentication method + /// Guest customer of the request; registration turns it into the new user + /// Store identifier /// URL to which the user will return after authentication /// Result of an authentication - protected virtual async Task RegisterNewUser(ExternalAuthParam parameters, string returnUrl) + protected virtual async Task RegisterNewUser(ExternalAuthParam parameters, Customer customer, + string storeId, string returnUrl) { var approved = _customerSettings.UserRegistrationType is UserRegistrationType.Standard or UserRegistrationType.EmailValidation; //create registration request - var registrationRequest = new RegistrationRequest(_contextAccessor.WorkContext.CurrentCustomer, + var registrationRequest = new RegistrationRequest(customer, parameters.Email, parameters.Email, CommonHelper.GenerateRandomDigitCode(20), PasswordFormat.Hashed, - _contextAccessor.StoreContext.CurrentStore.Id, + storeId, approved); //whether registration request has been completed successfully await _customerManagerService.RegisterCustomer(registrationRequest); //allow to save other customer values by consuming this event - await _mediator.Publish(new RegisteredByExternalMethod(_contextAccessor.WorkContext.CurrentCustomer, parameters)); + await _mediator.Publish(new RegisteredByExternalMethod(customer, parameters)); //raise customer registered event - await _mediator.Publish(new CustomerRegisteredEvent(_contextAccessor.WorkContext.CurrentCustomer)); + await _mediator.Publish(new CustomerRegisteredEvent(customer)); //associate external account with registered user - await AssociateCustomer(_contextAccessor.WorkContext.CurrentCustomer, parameters); + await AssociateCustomer(customer, parameters); //authenticate if (!approved) @@ -155,7 +158,7 @@ _customerSettings.UserRegistrationType is UserRegistrationType.Standard ? new RedirectToRouteResult("RegisterResult", new { resultId = (int)UserRegistrationType.AdminApproval }) : Error(["Error on registration"]); - await _authenticationService.SignIn(_contextAccessor.WorkContext.CurrentCustomer, false); + await _authenticationService.SignIn(customer, false); return new RedirectToRouteResult("RegisterResult", new { resultId = (int)UserRegistrationType.Standard }); } @@ -164,13 +167,15 @@ _customerSettings.UserRegistrationType is UserRegistrationType.Standard /// Login passed user /// /// User to login + /// Customer of the request before the login (the guest whose cart is merged) + /// Store identifier /// URL to which the user will return after authentication /// Result of an authentication - protected virtual async Task LoginUser(Customer user, string returnUrl) + protected virtual async Task LoginUser(Customer user, Customer previousCustomer, string storeId, + string returnUrl) { - //raise event - await _mediator.Publish(new CustomerLoggedInEvent(user, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); + //raise event + await _mediator.Publish(new CustomerLoggedInEvent(user, previousCustomer, storeId)); //authenticate await _authenticationService.SignIn(user, false); @@ -201,14 +206,17 @@ protected virtual IActionResult Error(IEnumerable errors) /// Load active external authentication providers /// /// External Authentication Providers - public virtual IList LoadActiveAuthenticationProviders(Customer customer = null, - Store store = null) + public virtual IList LoadActiveAuthenticationProviders(Customer customer, + Store store) { + ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(store); + return LoadAllAuthenticationProviders() .Where(provider => provider.IsMethodActive(_externalAuthenticationSettings) && - provider.IsAuthenticateGroup(_contextAccessor.WorkContext.CurrentCustomer) && - provider.IsAuthenticateStore(_contextAccessor.StoreContext.CurrentStore) + provider.IsAuthenticateGroup(customer) && + provider.IsAuthenticateStore(store) ).ToList(); } @@ -236,15 +244,20 @@ public virtual IList LoadAllAuthenticationProvi /// Check whether authentication by the passed external authentication provider is available /// /// System name of the external authentication provider + /// Customer whose groups the provider may be limited to + /// Store the provider may be limited to /// True if is available; otherwise false - public virtual bool AuthenticationProviderIsAvailable(string systemName) + public virtual bool AuthenticationProviderIsAvailable(string systemName, Customer customer, Store store) { + ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(store); + var authenticationMethod = LoadAuthenticationProviderBySystemName(systemName); return authenticationMethod != null && authenticationMethod.IsMethodActive(_externalAuthenticationSettings) && - authenticationMethod.IsAuthenticateGroup(_contextAccessor.WorkContext.CurrentCustomer) && - authenticationMethod.IsAuthenticateStore(_contextAccessor.StoreContext.CurrentStore); + authenticationMethod.IsAuthenticateGroup(customer) && + authenticationMethod.IsAuthenticateStore(store); } #endregion @@ -255,27 +268,30 @@ public virtual bool AuthenticationProviderIsAvailable(string systemName) /// Authenticate user by passed parameters /// /// External authentication parameters + /// Customer of the request (a guest when nobody is logged in) + /// Store of the request /// URL to which the user will return after authentication /// Result of an authentication - public virtual async Task Authenticate(ExternalAuthParam parameters, string returnUrl = null) + public virtual async Task Authenticate(ExternalAuthParam parameters, Customer customer, Store store, + string returnUrl = null) { ArgumentNullException.ThrowIfNull(parameters); + ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(store); - if (!AuthenticationProviderIsAvailable(parameters.ProviderSystemName)) + if (!AuthenticationProviderIsAvailable(parameters.ProviderSystemName, customer, store)) return Error(["External authentication method cannot be loaded"]); //get current logged-in user - var currentLoggedInUser = await _groupService.IsRegistered(_contextAccessor.WorkContext.CurrentCustomer) - ? _contextAccessor.WorkContext.CurrentCustomer - : null; + var currentLoggedInUser = await _groupService.IsRegistered(customer) ? customer : null; //authenticate associated user if already exists var associatedUser = await GetCustomer(parameters); if (associatedUser != null) - return await AuthenticateExistingUser(associatedUser, currentLoggedInUser, returnUrl); + return await AuthenticateExistingUser(associatedUser, currentLoggedInUser, customer, store.Id, returnUrl); //or associate and authenticate new user - return await AuthenticateNewUser(currentLoggedInUser, parameters, returnUrl); + return await AuthenticateNewUser(currentLoggedInUser, parameters, customer, store.Id, returnUrl); } #endregion diff --git a/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs b/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs index 9df583b322..d9037a6936 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs @@ -16,9 +16,12 @@ public interface IExternalAuthenticationService /// Use passed parameters to authenticate customer /// /// External authentication parameters + /// Customer of the request (a guest when nobody is logged in) + /// Store of the request /// Defines the URL of user return, when successfully authenticated /// Result of an authentication - Task Authenticate(ExternalAuthParam parameters, string returnUrl = null); + Task Authenticate(ExternalAuthParam parameters, Customer customer, Store store, + string returnUrl = null); #endregion @@ -50,11 +53,10 @@ public interface IExternalAuthenticationService /// /// Load all active authentication providers /// - /// Customer - /// Store + /// Customer whose groups the providers may be limited to + /// Store the providers may be limited to /// External authentication providers - IList LoadActiveAuthenticationProviders(Customer customer = null, - Store store = null); + IList LoadActiveAuthenticationProviders(Customer customer, Store store); /// /// Load specified external authentication provider by system name @@ -73,8 +75,10 @@ IList LoadActiveAuthenticationProviders(Custome /// Check whether authentication by the passed external authentication provider is available /// /// System name of the external authentication provider + /// Customer whose groups the provider may be limited to + /// Store the provider may be limited to /// True if is available; otherwise false - bool AuthenticationProviderIsAvailable(string systemName); + bool AuthenticationProviderIsAvailable(string systemName, Customer customer, Store store); #endregion } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Customers/CustomerExtensions.cs b/src/Core/Grand.Domain/Customers/CustomerExtensions.cs index b49a329b27..e101cd1f30 100644 --- a/src/Core/Grand.Domain/Customers/CustomerExtensions.cs +++ b/src/Core/Grand.Domain/Customers/CustomerExtensions.cs @@ -232,12 +232,12 @@ public static bool IsPasswordRecoveryLinkExpired(this Customer customer, Custome /// Get customer group identifiers /// /// Customer - /// Customer group identifiers + /// Customer group identifiers, sorted so that cache keys built from them do not depend on the stored order public static string[] GetCustomerGroupIds(this Customer customer) { ArgumentNullException.ThrowIfNull(customer); - return customer.Groups.ToArray(); + return customer.Groups.Order(StringComparer.Ordinal).ToArray(); } #endregion diff --git a/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs b/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs index 872797afc6..0e72bc1adc 100644 --- a/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs +++ b/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs @@ -1,6 +1,7 @@ using Authentication.Facebook.Models; using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Utilities.Authentication; +using Grand.Infrastructure; using Grand.SharedKernel; using Grand.Web.Common.Controllers; using Microsoft.AspNetCore.Authentication; @@ -17,10 +18,12 @@ public class FacebookAuthenticationController : BasePluginController public FacebookAuthenticationController( IExternalAuthenticationService externalAuthenticationService, - IConfiguration configuration) + IConfiguration configuration, + IContextAccessor contextAccessor) { _externalAuthenticationService = externalAuthenticationService; _configuration = configuration; + _contextAccessor = contextAccessor; } #endregion @@ -29,6 +32,7 @@ public FacebookAuthenticationController( private readonly IExternalAuthenticationService _externalAuthenticationService; private readonly IConfiguration _configuration; + private readonly IContextAccessor _contextAccessor; #endregion @@ -36,8 +40,9 @@ public FacebookAuthenticationController( public IActionResult FacebookLogin(string returnUrl) { - if (!_externalAuthenticationService.AuthenticationProviderIsAvailable(FacebookAuthenticationDefaults - .ProviderSystemName)) + if (!_externalAuthenticationService.AuthenticationProviderIsAvailable( + FacebookAuthenticationDefaults.ProviderSystemName, _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore)) throw new GrandException("Facebook authentication module cannot be loaded"); if (string.IsNullOrEmpty(_configuration["FacebookSettings:AppId"]) || @@ -71,7 +76,8 @@ public async Task FacebookLoginCallback(string returnUrl) }; //authenticate Grand user - return await _externalAuthenticationService.Authenticate(authenticationParameters, returnUrl); + return await _externalAuthenticationService.Authenticate(authenticationParameters, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, returnUrl); } public IActionResult FacebookSignInFailed(string error_code, string error_message, string state) diff --git a/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs b/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs index 9cc8fe1941..368c4acdff 100644 --- a/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs +++ b/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs @@ -1,6 +1,7 @@ using Authentication.Google.Models; using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Utilities.Authentication; +using Grand.Infrastructure; using Grand.SharedKernel; using Grand.Web.Common.Controllers; using Microsoft.AspNetCore.Authentication; @@ -16,10 +17,12 @@ public class GoogleAuthenticationController : BasePluginController #region Ctor public GoogleAuthenticationController(IExternalAuthenticationService externalAuthenticationService, - IConfiguration configuration) + IConfiguration configuration, + IContextAccessor contextAccessor) { _externalAuthenticationService = externalAuthenticationService; _configuration = configuration; + _contextAccessor = contextAccessor; } #endregion @@ -28,6 +31,7 @@ public GoogleAuthenticationController(IExternalAuthenticationService externalAut private readonly IExternalAuthenticationService _externalAuthenticationService; private readonly IConfiguration _configuration; + private readonly IContextAccessor _contextAccessor; #endregion @@ -35,8 +39,9 @@ public GoogleAuthenticationController(IExternalAuthenticationService externalAut public IActionResult GoogleLogin(string returnUrl) { - if (!_externalAuthenticationService.AuthenticationProviderIsAvailable(GoogleAuthenticationDefaults - .ProviderSystemName)) + if (!_externalAuthenticationService.AuthenticationProviderIsAvailable( + GoogleAuthenticationDefaults.ProviderSystemName, _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore)) throw new GrandException("Google authentication module cannot be loaded"); if (string.IsNullOrEmpty(_configuration["GoogleSettings:ClientId"]) || @@ -70,7 +75,8 @@ public async Task GoogleLoginCallback(string returnUrl) }; //authenticate grand user - return await _externalAuthenticationService.Authenticate(authenticationParameters, returnUrl); + return await _externalAuthenticationService.Authenticate(authenticationParameters, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, returnUrl); } public IActionResult GoogleSignInFailed(string error_message) diff --git a/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs b/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs index 658376972a..216949c58d 100644 --- a/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs +++ b/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs @@ -259,7 +259,8 @@ private async Task RefuseLinkInProviderOnly() } private bool IsAvailable() => - _externalAuthenticationService.AuthenticationProviderIsAvailable(OidcLoginDefaults.ProviderSystemName); + _externalAuthenticationService.AuthenticationProviderIsAvailable(OidcLoginDefaults.ProviderSystemName, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); private static bool TryReadMarker(string marker, out long shown, out string subject) { diff --git a/src/Plugins/Authentication.OpenIdConnect/Infrastructure/OidcSignOutProvider.cs b/src/Plugins/Authentication.OpenIdConnect/Infrastructure/OidcSignOutProvider.cs index c890d9abca..78281a8422 100644 --- a/src/Plugins/Authentication.OpenIdConnect/Infrastructure/OidcSignOutProvider.cs +++ b/src/Plugins/Authentication.OpenIdConnect/Infrastructure/OidcSignOutProvider.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Interfaces.Authentication; +using Grand.Infrastructure; using Grand.Infrastructure.Configuration; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.Logging; @@ -14,11 +15,14 @@ public class OidcSignOutProvider( IOptionsMonitor optionsMonitor, OidcLoginConfig config, IExternalAuthenticationService externalAuthenticationService, + IContextAccessor contextAccessor, ILogger logger) : IExternalSignOutProvider { public async Task GetSignOutUrl(string postLogoutRedirectUri) { - if (!externalAuthenticationService.AuthenticationProviderIsAvailable(OidcLoginDefaults.ProviderSystemName)) + //called before the local sign-out: the customer is still the signed-in one + if (!externalAuthenticationService.AuthenticationProviderIsAvailable(OidcLoginDefaults.ProviderSystemName, + contextAccessor.WorkContext.CurrentCustomer, contextAccessor.StoreContext.CurrentStore)) return null; var options = optionsMonitor.Get(OidcLoginDefaults.AuthenticationScheme); diff --git a/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs b/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs index 368ad2e18b..d79263934b 100644 --- a/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs +++ b/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs @@ -11,6 +11,7 @@ using Payments.BrainTree.Validators; using System.Net; using Environment = Braintree.Environment; +using Grand.Web.Common.Extensions; namespace Payments.BrainTree.Controllers; @@ -41,7 +42,7 @@ public async Task PaymentInfo() if (_brainTreePaymentSettings.Use3DS) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart); if (!cart.Any()) diff --git a/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs b/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs index ec0ed70820..065e400a0f 100644 --- a/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs +++ b/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs @@ -1,13 +1,14 @@ using Grand.Business.Authentication.Services; +using Grand.Business.Core.Events.Customers; using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Utilities.Authentication; +using Grand.Business.Core.Utilities.Customers; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Customers; using Grand.Domain.Stores; -using Grand.Infrastructure; using Grand.Mediator; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -26,7 +27,6 @@ public class ExternalAuthenticationServiceTests private ExternalAuthenticationService _externalAuthenticationService; private Mock _groupServiceMock; private Mock _mediatorMock; - private Mock _workContextMock; [TestInitialize] public void Init() @@ -37,9 +37,6 @@ public void Init() _customerManagerServiceMock = new Mock(); _mediatorMock = new Mock(); _externalAuthenticationRecordRepository = new MongoDBRepositoryTest(); - _workContextMock = new Mock(); - _workContextMock.Setup(s => s.WorkContext.CurrentCustomer).Returns(new Customer()); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); _externalAuthenticationProviders = new List { new ExternalAuthenticationProviderTest() }; @@ -52,8 +49,8 @@ public void Init() _externalAuthenticationService = new ExternalAuthenticationService(_authenticationServiceMock.Object, _customerManagerServiceMock.Object, _customerServiceMock.Object, _groupServiceMock.Object, _mediatorMock.Object, _externalAuthenticationRecordRepository, - _workContextMock.Object, _externalAuthenticationProviders, - new CustomerSettings(), externalAuthenticationSetting); + _externalAuthenticationProviders, + new CustomerSettings { UserRegistrationType = UserRegistrationType.Standard }, externalAuthenticationSetting); } @@ -61,7 +58,7 @@ public void Init() public void LoadActiveAuthenticationProvidersTest() { //Act - var result = _externalAuthenticationService.LoadActiveAuthenticationProviders(); + var result = _externalAuthenticationService.LoadActiveAuthenticationProviders(new Customer(), new Store()); //Assert Assert.IsTrue(result.Any()); } @@ -90,7 +87,8 @@ public void AuthenticationProviderIsAvailableTest_Available() { //Act var result = - _externalAuthenticationService.AuthenticationProviderIsAvailable("ExternalAuthenticationProviderTest"); + _externalAuthenticationService.AuthenticationProviderIsAvailable("ExternalAuthenticationProviderTest", + new Customer(), new Store()); //Assert Assert.IsTrue(result); } @@ -99,11 +97,65 @@ public void AuthenticationProviderIsAvailableTest_Available() public void AuthenticationProviderIsAvailableTest_NotAvailable() { //Act - var result = _externalAuthenticationService.AuthenticationProviderIsAvailable("123"); + var result = _externalAuthenticationService.AuthenticationProviderIsAvailable("123", new Customer(), new Store()); //Assert Assert.IsFalse(result); } + [TestMethod] + public void LoadActiveAuthenticationProviders_NullCustomerOrStore_Throws() + { + Assert.ThrowsExactly(() => + _externalAuthenticationService.LoadActiveAuthenticationProviders(null, new Store())); + Assert.ThrowsExactly(() => + _externalAuthenticationService.LoadActiveAuthenticationProviders(new Customer(), null)); + } + + [TestMethod] + public void AuthenticationProviderIsAvailable_NullCustomerOrStore_Throws() + { + Assert.ThrowsExactly(() => + _externalAuthenticationService.AuthenticationProviderIsAvailable("ExternalAuthenticationProviderTest", + null, new Store())); + Assert.ThrowsExactly(() => + _externalAuthenticationService.AuthenticationProviderIsAvailable("ExternalAuthenticationProviderTest", + new Customer(), null)); + } + + [TestMethod] + public async Task Authenticate_AssociatedUserFromGuest_PublishesLoggedInEventWithPassedCustomerAndStore() + { + var user = new Customer { Id = "user" }; + var guest = new Customer { Id = "guest" }; + _customerServiceMock.Setup(c => c.GetCustomerById("user")).ReturnsAsync(user); + var parameters = new ExternalAuthParam { ProviderSystemName = "ExternalAuthenticationProviderTest", Identifier = "1" }; + await _externalAuthenticationService.AssociateCustomer(user, parameters); + + await _externalAuthenticationService.Authenticate(parameters, guest, new Store { Id = "store1" }); + + _mediatorMock.Verify(m => m.Publish(It.Is(e => + e.Customer == user && e.PreviousCustomer == guest && e.StoreId == "store1"), default), Times.Once); + _authenticationServiceMock.Verify(a => a.SignIn(user, false), Times.Once); + } + + [TestMethod] + public async Task Authenticate_UnknownIdentity_RegistersPassedCustomerInPassedStore() + { + var guest = new Customer { Id = "guest" }; + var parameters = new ExternalAuthParam { + ProviderSystemName = "ExternalAuthenticationProviderTest", Identifier = "new", Email = "new@test.com" + }; + + await _externalAuthenticationService.Authenticate(parameters, guest, new Store { Id = "store1" }); + + _customerManagerServiceMock.Verify(m => m.RegisterCustomer(It.Is(r => + r.Customer == guest && r.StoreId == "store1" && r.Email == "new@test.com")), Times.Once); + _mediatorMock.Verify(m => m.Publish(It.Is(e => e.Customer == guest), default), + Times.Once); + _authenticationServiceMock.Verify(a => a.SignIn(guest, false), Times.Once); + Assert.AreEqual("guest", _externalAuthenticationRecordRepository.Table.Single().CustomerId); + } + [TestMethod] public async Task AssociateCustomerTest() { diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs index 8760991d54..8154afe8b0 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs @@ -76,4 +76,18 @@ public async Task CustomersOfDifferentGroups_DoNotShareTheCachedTree() Assert.AreNotSame(first, second, "the ACL filter differs per group, so the entries must differ"); } + + [TestMethod] + public async Task SameGroupsInAnotherOrder_ShareTheCachedTree() + { + await _categoryService.InsertCategory(new Category { ParentCategoryId = "root", Published = true }); + + var first = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(WithGroups("customer-1", "group-a", "group-b"), "store"), "root"); + + var second = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(WithGroups("customer-2", "group-b", "group-a"), "store"), "root"); + + Assert.AreSame(first, second, "the order the groups are stored in must not split the cache"); + } } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs index 573c44188e..2842689096 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs @@ -145,4 +145,27 @@ public async Task GetAllCheckoutAttributes_CacheKeyDependsOnTheCustomerGroups() //Assert Assert.AreNotEqual(keys[0], keys[1]); } + + [TestMethod] + public async Task GetAllCheckoutAttributes_SameGroupsInAnotherOrder_UseTheSameCacheKey() + { + //Arrange + var keys = new List(); + _cacheMock.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) + .Callback((string key, Func>> _) => keys.Add(key)) + .ReturnsAsync(new List()); + var first = new Customer(); + first.Groups.Add("registered"); + first.Groups.Add("members"); + var second = new Customer(); + second.Groups.Add("members"); + second.Groups.Add("registered"); + + //Act + await _service.GetAllCheckoutAttributes(CatalogAccess.For(first, "store")); + await _service.GetAllCheckoutAttributes(CatalogAccess.For(second, "store")); + + //Assert + Assert.AreEqual(keys[0], keys[1]); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Domain.Tests/Customers/CustomerExtensionsTests.cs b/src/Tests/Grand.Domain.Tests/Customers/CustomerExtensionsTests.cs new file mode 100644 index 0000000000..f4d477a98f --- /dev/null +++ b/src/Tests/Grand.Domain.Tests/Customers/CustomerExtensionsTests.cs @@ -0,0 +1,33 @@ +using Grand.Domain.Customers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Domain.Tests.Customers; + +[TestClass] +public class CustomerExtensionsTests +{ + [TestMethod] + public void GetCustomerGroupIds_ReturnsTheGroupsSorted() + { + var customer = new Customer(); + customer.Groups.Add("registered"); + customer.Groups.Add("Members"); + customer.Groups.Add("guests"); + + var ids = customer.GetCustomerGroupIds(); + + CollectionAssert.AreEqual(new[] { "Members", "guests", "registered" }, ids); + } + + [TestMethod] + public void GetCustomerGroupIds_DoesNotReorderTheCustomerGroups() + { + var customer = new Customer(); + customer.Groups.Add("registered"); + customer.Groups.Add("guests"); + + customer.GetCustomerGroupIds(); + + CollectionAssert.AreEqual(new[] { "registered", "guests" }, customer.Groups.ToArray()); + } +} diff --git a/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs b/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs index 5620f56c5a..cdef82939b 100644 --- a/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs +++ b/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs @@ -77,7 +77,7 @@ public void Init() _sessions.Setup(x => x.Start(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync("session-1"); _mediator = new Mock(); _externalAuth = new Mock(); - _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(OidcLoginDefaults.ProviderSystemName)).Returns(true); + _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(OidcLoginDefaults.ProviderSystemName, It.IsAny(), It.IsAny())).Returns(true); _schemes = new Mock(); _schemes.Setup(x => x.GetSchemeAsync(OidcLoginDefaults.AuthenticationScheme)).ReturnsAsync( new AuthenticationScheme(OidcLoginDefaults.AuthenticationScheme, null, typeof(IAuthenticationHandler))); @@ -149,7 +149,7 @@ public async Task Login_NotConfigured_Failed() [TestMethod] public async Task Login_MethodInactive_Failed() { - _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny())).Returns(false); + _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny(), It.IsAny(), It.IsAny())).Returns(false); Assert.AreEqual("OidcLoginFailed", ((RedirectToRouteResult)await _controller.Login("/")).RouteName); } @@ -286,7 +286,7 @@ public void Failed_IdentityTaken_InModel() public async Task Callback_MethodInactive_ClearsAndFails_NothingResolved() { _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny())).Returns(false); + _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny(), It.IsAny(), It.IsAny())).Returns(false); Assert.AreEqual("OidcLoginFailed", ((RedirectToRouteResult)await _controller.Callback("/")).RouteName); _external.Verify(x => x.Clear(), Times.Once); _resolver.Verify(x => x.ResolveForLogin(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), @@ -308,7 +308,7 @@ public async Task Link_MethodInactive_ClearsAndFails_StaysSignedIn() { _groups.Setup(x => x.IsRegistered(_current)).ReturnsAsync(true); _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); - _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny())).Returns(false); + _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny(), It.IsAny(), It.IsAny())).Returns(false); Assert.AreEqual("OidcLoginFailed", ((RedirectToRouteResult)await _controller.Link("/")).RouteName); _external.Verify(x => x.Clear(), Times.Once); _httpAuth.Verify(x => x.SignOutAsync(It.IsAny(), GrandCookieAuthenticationDefaults.AuthenticationScheme, @@ -321,7 +321,7 @@ public async Task LinkComplete_MethodInactive_ClearsAndFails_NothingLinked() _groups.Setup(x => x.IsRegistered(_current)).ReturnsAsync(true); _external.Setup(x => x.GetPending()).ReturnsAsync(Pending); Marker("sub-1"); - _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny())).Returns(false); + _externalAuth.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny(), It.IsAny(), It.IsAny())).Returns(false); Assert.AreEqual("OidcLoginFailed", ((RedirectToRouteResult)await _controller.LinkComplete("/")).RouteName); _external.Verify(x => x.Complete(It.IsAny(), It.IsAny()), Times.Never); _external.Verify(x => x.Clear(), Times.Once); diff --git a/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcSignOutProviderTests.cs b/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcSignOutProviderTests.cs index 6bcafb76cb..17499096b3 100644 --- a/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcSignOutProviderTests.cs +++ b/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcSignOutProviderTests.cs @@ -1,6 +1,7 @@ using Authentication.OpenIdConnect; using Authentication.OpenIdConnect.Infrastructure; using Grand.Business.Core.Interfaces.Authentication; +using Grand.Infrastructure; using Grand.Infrastructure.Configuration; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.Logging; @@ -25,7 +26,7 @@ public class OidcSignOutProviderTests public void Init() { _authenticationService = new Mock(); - _authenticationService.Setup(x => x.AuthenticationProviderIsAvailable(OidcLoginDefaults.ProviderSystemName)) + _authenticationService.Setup(x => x.AuthenticationProviderIsAvailable(OidcLoginDefaults.ProviderSystemName, It.IsAny(), It.IsAny())) .Returns(true); _manager = new Mock>(); _logger = new Mock>(); @@ -43,7 +44,7 @@ private OidcSignOutProvider Provider(bool withManager = true) ConfigurationManager = withManager ? _manager.Object : null }); return new OidcSignOutProvider(monitor.Object, new OidcLoginConfig { ClientId = "shop app" }, - _authenticationService.Object, _logger.Object); + _authenticationService.Object, new Mock { DefaultValue = DefaultValue.Mock }.Object, _logger.Object); } [TestMethod] @@ -76,7 +77,7 @@ public async Task NoConfigurationManager_Null() => [TestMethod] public async Task NotAvailable_Null() { - _authenticationService.Setup(x => x.AuthenticationProviderIsAvailable(OidcLoginDefaults.ProviderSystemName)) + _authenticationService.Setup(x => x.AuthenticationProviderIsAvailable(OidcLoginDefaults.ProviderSystemName, It.IsAny(), It.IsAny())) .Returns(false); Assert.IsNull(await Provider().GetSignOutUrl(Back)); _manager.Verify(x => x.GetConfigurationAsync(It.IsAny()), Times.Never); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs index 596c46c5c7..e16c8d1879 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs @@ -34,7 +34,7 @@ public void Setup() _controller = new LoginController(new CustomerSettings(), new CaptchaSettings(), new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, - new Mock().Object, new Mock().Object, + new Mock().Object, new Mock { DefaultValue = DefaultValue.Mock }.Object, new Mock().Object); var httpContext = new DefaultHttpContext(); httpContext.Request.PathBase = "/shop"; @@ -54,7 +54,7 @@ public async Task Index_ListsPanelProvidersOnly() panel.Setup(x => x.FriendlyName).Returns("OpenID Connect"); panel.Setup(x => x.GetSignInPath("/shop/admin")).ReturnsAsync("/oidc/login?returnUrl=%2Fshop%2Fadmin"); var storefrontOnly = new Mock(); - _externalAuthenticationService.Setup(x => x.LoadActiveAuthenticationProviders(null, null)) + _externalAuthenticationService.Setup(x => x.LoadActiveAuthenticationProviders(It.IsAny(), It.IsAny())) .Returns([panel.Object, storefrontOnly.Object]); var model = (LoginModel)((ViewResult)await _controller.Index(_externalAuthenticationService.Object, new OidcLoginConfig(), _groupService.Object)).Model!; @@ -69,7 +69,7 @@ public async Task Index_ProviderWithoutPath_Hidden() { var panel = new Mock(); panel.Setup(x => x.GetSignInPath(It.IsAny())).ReturnsAsync((string)null!); - _externalAuthenticationService.Setup(x => x.LoadActiveAuthenticationProviders(null, null)).Returns([panel.Object]); + _externalAuthenticationService.Setup(x => x.LoadActiveAuthenticationProviders(It.IsAny(), It.IsAny())).Returns([panel.Object]); var model = (LoginModel)((ViewResult)await _controller.Index(_externalAuthenticationService.Object, new OidcLoginConfig(), _groupService.Object)).Model!; Assert.AreEqual(0, model.ExternalSignIn.Count); } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs index 0316220bf6..ac8ce24d61 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs @@ -34,7 +34,7 @@ public void Setup() _groupService = new Mock(); var workContext = new Mock(); workContext.Setup(x => x.CurrentCustomer).Returns(new Customer()); - _contextAccessor = new Mock(); + _contextAccessor = new Mock { DefaultValue = DefaultValue.Mock }; _contextAccessor.Setup(x => x.WorkContext).Returns(workContext.Object); _customerManagerService = new Mock(); } @@ -63,7 +63,7 @@ private void WithSignInPath(string? path) var provider = new Mock(); provider.Setup(x => x.FriendlyName).Returns("OpenID Connect"); provider.Setup(x => x.GetSignInPath(It.IsAny())).ReturnsAsync(path); - _externalAuthenticationService.Setup(x => x.LoadActiveAuthenticationProviders(null, null)) + _externalAuthenticationService.Setup(x => x.LoadActiveAuthenticationProviders(It.IsAny(), It.IsAny())) .Returns([provider.Object]); } diff --git a/src/Tests/Grand.Web.Common.Tests/Extensions/ShoppingCartServiceExtensionsTests.cs b/src/Tests/Grand.Web.Common.Tests/Extensions/ShoppingCartServiceExtensionsTests.cs new file mode 100644 index 0000000000..06a310b5a7 --- /dev/null +++ b/src/Tests/Grand.Web.Common.Tests/Extensions/ShoppingCartServiceExtensionsTests.cs @@ -0,0 +1,35 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Domain.Stores; +using Grand.Infrastructure; +using Grand.Web.Common.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Common.Tests.Extensions; + +[TestClass] +public class ShoppingCartServiceExtensionsTests +{ + [TestMethod] + public async Task GetCurrentCart_ReadsTheCartOfTheRequestCustomerInTheRequestStore() + { + //Arrange + var customer = new Customer(); + var accessor = new Mock(); + accessor.Setup(a => a.WorkContext.CurrentCustomer).Returns(customer); + accessor.Setup(a => a.StoreContext.CurrentStore).Returns(new Store { Id = "s1" }); + IList items = [new ShoppingCartItem()]; + var service = new Mock(); + service.Setup(s => s.GetShoppingCart(customer, "s1", ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions)) + .ReturnsAsync(items); + + //Act + var result = await service.Object.GetCurrentCart(accessor.Object, ShoppingCartType.ShoppingCart, + ShoppingCartType.Auctions); + + //Assert + Assert.AreSame(items, result); + } +} diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/Customers/GetInfoHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/Customers/GetInfoHandlerTests.cs index 18910ba29b..45db0fbf5f 100644 --- a/src/Tests/Grand.Web.Tests/Features/Handlers/Customers/GetInfoHandlerTests.cs +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/Customers/GetInfoHandlerTests.cs @@ -33,7 +33,7 @@ private static async Task Handle(OidcLoginConfig config) new() { Id = "1", ProviderSystemName = Oidc }, new() { Id = "2", ProviderSystemName = Google } }); - external.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny())).Returns(true); + external.Setup(x => x.AuthenticationProviderIsAvailable(It.IsAny(), It.IsAny(), It.IsAny())).Returns(true); var providerMock = new Mock(); providerMock.Setup(p => p.FriendlyName).Returns("Provider"); external.Setup(x => x.LoadAuthenticationProviderBySystemName(It.IsAny())).Returns(providerMock.Object); diff --git a/src/Tests/Grand.Web.Tests/Validators/ShoppingCart/UpdateQuantityValidatorTests.cs b/src/Tests/Grand.Web.Tests/Validators/ShoppingCart/UpdateQuantityValidatorTests.cs new file mode 100644 index 0000000000..488c2a4b82 --- /dev/null +++ b/src/Tests/Grand.Web.Tests/Validators/ShoppingCart/UpdateQuantityValidatorTests.cs @@ -0,0 +1,92 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Domain.Permissions; +using Grand.Infrastructure; +using Grand.Infrastructure.Validators; +using Grand.Web.Models.ShoppingCart; +using Grand.Web.Validators.ShoppingCart; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Tests.Validators.ShoppingCart; + +[TestClass] +public class UpdateQuantityValidatorTests +{ + private static UpdateQuantityValidator Validator(ShoppingCartItem item, bool cartEnabled, bool wishlistEnabled) + { + var permissionService = new Mock(); + permissionService.Setup(p => p.Authorize(It.Is(x => + x.SystemName == StandardPermission.EnableShoppingCart.SystemName))) + .ReturnsAsync(cartEnabled); + permissionService.Setup(p => p.Authorize(It.Is(x => + x.SystemName == StandardPermission.EnableWishlist.SystemName))) + .ReturnsAsync(wishlistEnabled); + + var shoppingCartService = new Mock(); + shoppingCartService.Setup(s => s.GetShoppingCart(It.IsAny(), It.IsAny(), + It.IsAny())) + .ReturnsAsync([item]); + + var work = new Mock(); + work.Setup(w => w.CurrentCustomer).Returns(new Customer()); + var store = new Mock(); + store.Setup(s => s.CurrentStore).Returns(new Domain.Stores.Store { Id = "s1" }); + var context = new Mock(); + context.Setup(c => c.WorkContext).Returns(work.Object); + context.Setup(c => c.StoreContext).Returns(store.Object); + + return new UpdateQuantityValidator(new List>(), + permissionService.Object, new ShoppingCartSettings(), shoppingCartService.Object, context.Object); + } + + [TestMethod] + public async Task CartItemDeclaredAsWishlist_WithoutCartPermission_IsRejected() + { + var item = new ShoppingCartItem { Id = "i1", ShoppingCartTypeId = ShoppingCartType.ShoppingCart }; + + var result = await Validator(item, false, true).ValidateAsync(new UpdateQuantityModel { + ShoppingCartId = "i1", Quantity = 5, ShoppingCartType = ShoppingCartType.Wishlist + }); + + Assert.IsFalse(result.IsValid); + } + + [TestMethod] + public async Task OnHoldItemDeclaredAsCart_WithCartPermission_IsValid() + { + var item = new ShoppingCartItem { Id = "i1", ShoppingCartTypeId = ShoppingCartType.OnHoldCart }; + + var result = await Validator(item, true, false).ValidateAsync(new UpdateQuantityModel { + ShoppingCartId = "i1", Quantity = 5, ShoppingCartType = ShoppingCartType.ShoppingCart + }); + + Assert.IsTrue(result.IsValid); + } + + [TestMethod] + public async Task WishlistItem_WithoutWishlistPermission_IsRejected() + { + var item = new ShoppingCartItem { Id = "i1", ShoppingCartTypeId = ShoppingCartType.Wishlist }; + + var result = await Validator(item, true, false).ValidateAsync(new UpdateQuantityModel { + ShoppingCartId = "i1", Quantity = 5, ShoppingCartType = ShoppingCartType.Wishlist + }); + + Assert.IsFalse(result.IsValid); + } + + [TestMethod] + public async Task UnknownItem_IsRejected() + { + var item = new ShoppingCartItem { Id = "i1", ShoppingCartTypeId = ShoppingCartType.ShoppingCart }; + + var result = await Validator(item, true, true).ValidateAsync(new UpdateQuantityModel { + ShoppingCartId = "other", Quantity = 5, ShoppingCartType = ShoppingCartType.ShoppingCart + }); + + Assert.IsFalse(result.IsValid); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs index 6dbde89931..0f25a00708 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs @@ -137,7 +137,9 @@ await _customerManagerService.LoginCustomer( private async Task PrepareExternalSignIn(LoginModel model, IExternalAuthenticationService externalAuthenticationService) { var returnUrl = Url.RouteUrl($"{GetCurrentArea()}Index"); - foreach (var provider in externalAuthenticationService.LoadActiveAuthenticationProviders() + foreach (var provider in externalAuthenticationService + .LoadActiveAuthenticationProviders(_contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore) .OfType()) { var path = await provider.GetSignInPath(returnUrl); diff --git a/src/Web/Grand.Web.Common/Extensions/ShoppingCartServiceExtensions.cs b/src/Web/Grand.Web.Common/Extensions/ShoppingCartServiceExtensions.cs new file mode 100644 index 0000000000..386291710e --- /dev/null +++ b/src/Web/Grand.Web.Common/Extensions/ShoppingCartServiceExtensions.cs @@ -0,0 +1,24 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Domain.Orders; +using Grand.Infrastructure; + +namespace Grand.Web.Common.Extensions; + +/// +/// Reads the shopping cart of the current request +/// +public static class ShoppingCartServiceExtensions +{ + /// + /// Gets the cart items of the current request: its customer in its store + /// + /// Shopping cart service + /// Context of the current request + /// Shopping cart types; pass none to load all records + public static Task> GetCurrentCart(this IShoppingCartService shoppingCartService, + IContextAccessor contextAccessor, params ShoppingCartType[] shoppingCartType) + { + return shoppingCartService.GetShoppingCart(contextAccessor.WorkContext.CurrentCustomer, + contextAccessor.StoreContext.CurrentStore.Id, shoppingCartType); + } +} diff --git a/src/Web/Grand.Web/Components/EmailWishlist.cs b/src/Web/Grand.Web/Components/EmailWishlist.cs index 51c2479324..774a069a7b 100644 --- a/src/Web/Grand.Web/Components/EmailWishlist.cs +++ b/src/Web/Grand.Web/Components/EmailWishlist.cs @@ -7,6 +7,7 @@ using Grand.Web.Common.Components; using Grand.Web.Models.ShoppingCart; using Microsoft.AspNetCore.Mvc; +using Grand.Web.Common.Extensions; namespace Grand.Web.Components; @@ -39,7 +40,7 @@ public async Task InvokeAsync() !_shoppingCartSettings.EmailWishlistEnabled) return Content(""); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.Wishlist); if (!cart.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/EstimateShipping.cs b/src/Web/Grand.Web/Components/EstimateShipping.cs index 83b59cf1fd..30424a5f66 100644 --- a/src/Web/Grand.Web/Components/EstimateShipping.cs +++ b/src/Web/Grand.Web/Components/EstimateShipping.cs @@ -5,6 +5,7 @@ using Grand.Web.Features.Models.ShoppingCart; using Grand.Mediator; using Microsoft.AspNetCore.Mvc; +using Grand.Web.Common.Extensions; namespace Grand.Web.Components; @@ -24,7 +25,7 @@ public EstimateShippingViewComponent(IMediator mediator, IShoppingCartService sh public async Task InvokeAsync() { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart); var model = await _mediator.Send(new GetEstimateShipping { diff --git a/src/Web/Grand.Web/Controllers/ActionCartController.cs b/src/Web/Grand.Web/Controllers/ActionCartController.cs index f40980bd54..5eeac268e1 100644 --- a/src/Web/Grand.Web/Controllers/ActionCartController.cs +++ b/src/Web/Grand.Web/Controllers/ActionCartController.cs @@ -18,6 +18,7 @@ using Grand.Mediator; using Microsoft.AspNetCore.Mvc; using System.Globalization; +using Grand.Web.Common.Extensions; namespace Grand.Web.Controllers; @@ -182,7 +183,7 @@ public virtual async Task> AddProductCatalog(Pr var warehouseId = GetWarehouse(product); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, model.ShoppingCartTypeId); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, model.ShoppingCartTypeId); if (model.ShoppingCartTypeId != ShoppingCartType.Wishlist) { @@ -255,7 +256,7 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, }); //display notification message and update appropriate blocks - var qty = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var qty = (await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.Wishlist)).Sum(x => x.Quantity); var updatetopwishlistsectionhtml = string.Format(_translationService.GetResource("Wishlist.HeaderQuantity"), qty); @@ -289,7 +290,7 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, var updatetopcartsectionhtml = string.Format( _translationService.GetResource("ShoppingCart.HeaderQuantity"), - (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + (await _shoppingCartService.GetCurrentCart(_contextAccessor, shoppingCartTypes.ToArray())) .Sum(x => x.Quantity)); @@ -451,7 +452,7 @@ public virtual async Task> AddProductDetails(Pr }); //display notification message and update appropriate blocks - var qty = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var qty = (await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.Wishlist)).Sum(x => x.Quantity); var updatetopwishlistsectionhtml = string.Format(_translationService.GetResource("Wishlist.HeaderQuantity"), qty); @@ -485,7 +486,7 @@ public virtual async Task> AddProductDetails(Pr var updatetopcartsectionhtml = string.Format( _translationService.GetResource("ShoppingCart.HeaderQuantity"), - (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + (await _shoppingCartService.GetCurrentCart(_contextAccessor, shoppingCartTypes.ToArray())) .Sum(x => x.Quantity)); diff --git a/src/Web/Grand.Web/Controllers/CheckoutController.cs b/src/Web/Grand.Web/Controllers/CheckoutController.cs index 8b62cb1bdb..72d3d3cca3 100644 --- a/src/Web/Grand.Web/Controllers/CheckoutController.cs +++ b/src/Web/Grand.Web/Controllers/CheckoutController.cs @@ -88,7 +88,7 @@ public virtual async Task Index() { var customer = _contextAccessor.WorkContext.CurrentCustomer; - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); if (!cart.Any()) @@ -125,7 +125,7 @@ await _shoppingCartValidator.GetShoppingCartItemWarnings(customer, sci, product, public virtual async Task Start() { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); if (!cart.Any()) @@ -191,7 +191,7 @@ public virtual async Task SaveBilling( try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -289,7 +289,7 @@ public virtual async Task SaveShipping( try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -465,7 +465,7 @@ public virtual async Task SaveShippingMethod(CheckoutShippingMeth var customer = _contextAccessor.WorkContext.CurrentCustomer; var store = _contextAccessor.StoreContext.CurrentStore; - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -550,7 +550,7 @@ public virtual async Task SavePaymentMethod(CheckoutPaymentMethod try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -620,7 +620,7 @@ public virtual async Task SavePaymentInfo(IDictionary ConfirmOrder() try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); diff --git a/src/Web/Grand.Web/Controllers/ProductController.cs b/src/Web/Grand.Web/Controllers/ProductController.cs index e3a19dc491..85791be6e7 100644 --- a/src/Web/Grand.Web/Controllers/ProductController.cs +++ b/src/Web/Grand.Web/Controllers/ProductController.cs @@ -239,7 +239,7 @@ public virtual async Task GetDatesForMonth(string productId, int if (!string.IsNullOrEmpty(parameter)) query = query.Where(x => x.Parameter == parameter); var reservations = query.ToList(); - var inCart = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)) + var inCart = (await _shoppingCartService.GetCurrentCart(_contextAccessor)) .Where(x => !string.IsNullOrEmpty(x.ReservationId)).ToList(); foreach (var cartItem in inCart) { diff --git a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs index c4d7369fbc..775b737b8a 100644 --- a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs @@ -68,6 +68,22 @@ public ShoppingCartController( #region Utilities + private GetShoppingCart ShoppingCartRequest(IList cart, bool isEditable = true, + bool validateCheckoutAttributes = false) + { + return new GetShoppingCart { + Cart = cart, + IsEditable = isEditable, + ValidateCheckoutAttributes = validateCheckoutAttributes, + Customer = _contextAccessor.WorkContext.CurrentCustomer, + Currency = _contextAccessor.WorkContext.WorkingCurrency, + Language = _contextAccessor.WorkContext.WorkingLanguage, + TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, + Store = _contextAccessor.StoreContext.CurrentStore, + Host = _contextAccessor.StoreContext.CurrentHost + }; + } + private ShoppingCartType[] PrepareCartTypes() { var shoppingCartTypes = new List { @@ -127,7 +143,7 @@ public virtual async Task CheckoutAttributeChange(CheckoutAttribu [FromServices] ICheckoutAttributeParser checkoutAttributeParser, [FromServices] ICheckoutAttributeFormatter checkoutAttributeFormatter) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var checkoutAttributes = await _mediator.Send(new SaveCheckoutAttributesCommand { @@ -253,16 +269,8 @@ public virtual async Task> Cart(bool checkoutAtt if (!await _permissionService.Authorize(StandardPermission.EnableShoppingCart)) return RedirectToRoute("HomePage"); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); - var model = await _mediator.Send(new GetShoppingCart { - Cart = cart, - ValidateCheckoutAttributes = checkoutAttributes, - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Currency = _contextAccessor.WorkContext.WorkingCurrency, - Language = _contextAccessor.WorkContext.WorkingLanguage, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType, - Store = _contextAccessor.StoreContext.CurrentStore - }); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes()); + var model = await _mediator.Send(ShoppingCartRequest(cart, validateCheckoutAttributes: checkoutAttributes)); return View(model); } @@ -270,18 +278,10 @@ public virtual async Task> Cart(bool checkoutAtt [DenySystemAccount] public async Task> CartSummary() { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); - var model = await _mediator.Send(new GetShoppingCart { - Cart = cart, - IsEditable = false, - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Currency = _contextAccessor.WorkContext.WorkingCurrency, - Language = _contextAccessor.WorkContext.WorkingLanguage, - Store = _contextAccessor.StoreContext.CurrentStore, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType - }); + var model = await _mediator.Send(ShoppingCartRequest(cart, isEditable: false)); return Json(model); } @@ -290,7 +290,7 @@ public async Task> CartSummary() [DenySystemAccount] public async Task> CartTotal() { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var model = await _mediator.Send(new GetOrderTotals { @@ -315,7 +315,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode warnings = string.Join(',', ModelState.Values.SelectMany(x => x.Errors.Select(x => x.ErrorMessage))) }); - var cart = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes())) + var cart = (await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes())) .FirstOrDefault(x => x.Id == model.ShoppingCartId); var warnings = new List(); @@ -328,14 +328,8 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode warnings.AddRange(currSciWarnings); } - var cartModel = await _mediator.Send(new GetShoppingCart { - Cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()), - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Currency = _contextAccessor.WorkContext.WorkingCurrency, - Language = _contextAccessor.WorkContext.WorkingLanguage, - Store = _contextAccessor.StoreContext.CurrentStore, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType - }); + var updatedCart = await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes()); + var cartModel = await _mediator.Send(ShoppingCartRequest(updatedCart)); return Json(new { success = !warnings.Any(), @@ -353,7 +347,7 @@ public virtual async Task ClearCart() if (!await _permissionService.Authorize(StandardPermission.EnableShoppingCart)) return RedirectToRoute("HomePage"); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes()); foreach (var sci in cart) await _shoppingCartService.DeleteShoppingCartItem(_contextAccessor.WorkContext.CurrentCustomer, sci, @@ -373,7 +367,7 @@ public virtual async Task DeleteCartItem(DeleteCartItemModel mode if (_shoppingCartSettings.AllowOnHoldCart) shoppingCartTypes.Add(ShoppingCartType.OnHoldCart); - var item = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var item = (await _shoppingCartService.GetCurrentCart(_contextAccessor, shoppingCartTypes.ToArray())) .FirstOrDefault(sci => sci.Id == model.Id); @@ -395,15 +389,8 @@ await _shoppingCartService.DeleteShoppingCartItem(_contextAccessor.WorkContext.C sidebarshoppingcartmodel = miniShoppingCartmodel }); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); - var shoppingcartmodel = await _mediator.Send(new GetShoppingCart { - Cart = cart, - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Currency = _contextAccessor.WorkContext.WorkingCurrency, - Language = _contextAccessor.WorkContext.WorkingLanguage, - Store = _contextAccessor.StoreContext.CurrentStore, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType - }); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes()); + var shoppingcartmodel = await _mediator.Send(ShoppingCartRequest(cart)); return Json(new { totalproducts = string.Format(_translationService.GetResource("ShoppingCart.HeaderQuantity"), @@ -427,7 +414,7 @@ public virtual async Task ChangeTypeCartItem(ChangeTypeCartItemMo if (_shoppingCartSettings.AllowOnHoldCart) shoppingCartTypes.Add(ShoppingCartType.OnHoldCart); - var item = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var item = (await _shoppingCartService.GetCurrentCart(_contextAccessor, shoppingCartTypes.ToArray())) .FirstOrDefault(sci => sci.Id == model.Id); @@ -445,15 +432,8 @@ public virtual async Task ChangeTypeCartItem(ChangeTypeCartItemMo Store = _contextAccessor.StoreContext.CurrentStore }); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); - var shoppingcartmodel = await _mediator.Send(new GetShoppingCart { - Cart = cart, - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Currency = _contextAccessor.WorkContext.WorkingCurrency, - Language = _contextAccessor.WorkContext.WorkingLanguage, - Store = _contextAccessor.StoreContext.CurrentStore, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType - }); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes()); + var shoppingcartmodel = await _mediator.Send(ShoppingCartRequest(cart)); return Json(new { model = shoppingcartmodel, @@ -472,7 +452,7 @@ public virtual IActionResult ContinueShopping() [DenySystemAccount] public virtual async Task StartCheckout(CheckoutAttributeSelectedModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); List checkoutAttributes; //parse and save checkout attributes @@ -505,7 +485,7 @@ await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, [HttpPost] public virtual async Task ApplyDiscountCoupon(DiscountCouponModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes()); var message = string.Empty; var isApplied = false; @@ -527,14 +507,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto message = string.Join(',', ModelState.Values.SelectMany(x => x.Errors.Select(x => x.ErrorMessage))); } - var cartModel = await _mediator.Send(new GetShoppingCart { - Cart = cart, - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Currency = _contextAccessor.WorkContext.WorkingCurrency, - Language = _contextAccessor.WorkContext.WorkingLanguage, - Store = _contextAccessor.StoreContext.CurrentStore, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType - }); + var cartModel = await _mediator.Send(ShoppingCartRequest(cart)); cartModel.DiscountBox.Message = message; cartModel.DiscountBox.IsApplied = isApplied; @@ -548,7 +521,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto [HttpPost] public virtual async Task ApplyGiftVoucher(GiftVoucherCouponModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes()); var message = string.Empty; var isApplied = false; @@ -571,14 +544,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto message = string.Join(',', ModelState.Values.SelectMany(x => x.Errors.Select(x => x.ErrorMessage))); } - var cartModel = await _mediator.Send(new GetShoppingCart { - Cart = cart, - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Currency = _contextAccessor.WorkContext.WorkingCurrency, - Language = _contextAccessor.WorkContext.WorkingLanguage, - Store = _contextAccessor.StoreContext.CurrentStore, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType - }); + var cartModel = await _mediator.Send(ShoppingCartRequest(cart)); cartModel.GiftVoucherBox.Message = message; cartModel.GiftVoucherBox.IsApplied = isApplied; @@ -591,7 +557,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto [HttpPost] public virtual async Task GetEstimateShipping(EstimateShippingModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var result = await _mediator.Send(new GetEstimateShippingResult { @@ -629,16 +595,9 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto } } - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes()); - var model = await _mediator.Send(new GetShoppingCart { - Cart = cart, - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Currency = _contextAccessor.WorkContext.WorkingCurrency, - Language = _contextAccessor.WorkContext.WorkingLanguage, - Store = _contextAccessor.StoreContext.CurrentStore, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType - }); + var model = await _mediator.Send(ShoppingCartRequest(cart)); return Json(new { model @@ -664,16 +623,9 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto } } - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, PrepareCartTypes()); - var model = await _mediator.Send(new GetShoppingCart { - Cart = cart, - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Currency = _contextAccessor.WorkContext.WorkingCurrency, - Language = _contextAccessor.WorkContext.WorkingLanguage, - Store = _contextAccessor.StoreContext.CurrentStore, - TaxDisplayType = _contextAccessor.WorkContext.TaxDisplayType - }); + var model = await _mediator.Send(ShoppingCartRequest(cart)); return Json(new { model diff --git a/src/Web/Grand.Web/Controllers/WishlistController.cs b/src/Web/Grand.Web/Controllers/WishlistController.cs index c6adf55a12..199bc649e7 100644 --- a/src/Web/Grand.Web/Controllers/WishlistController.cs +++ b/src/Web/Grand.Web/Controllers/WishlistController.cs @@ -16,6 +16,7 @@ using Grand.Web.Models.ShoppingCart; using Grand.Mediator; using Microsoft.AspNetCore.Mvc; +using Grand.Web.Common.Extensions; namespace Grand.Web.Controllers; @@ -64,14 +65,10 @@ public async Task> SidebarWishlist() if (!await _permissionService.Authorize(StandardPermission.EnableWishlist)) return Content(""); - var cart = _contextAccessor.WorkContext.CurrentCustomer.ShoppingCartItems.Where(sci => - sci.ShoppingCartTypeId == ShoppingCartType.Wishlist); - - if (!string.IsNullOrEmpty(_contextAccessor.StoreContext.CurrentStore.Id)) - cart = cart.LimitPerStore(_shoppingCartSettings.SharedCartBetweenStores, _contextAccessor.StoreContext.CurrentStore.Id); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.Wishlist); var model = await _mediator.Send(new GetMiniWishlist { - Cart = cart.ToList(), + Cart = cart, Customer = _contextAccessor.WorkContext.CurrentCustomer, Language = _contextAccessor.WorkContext.WorkingLanguage, Currency = _contextAccessor.WorkContext.WorkingCurrency, @@ -93,13 +90,11 @@ public virtual async Task> Index(Guid? customerGuid) if (customer == null) return RedirectToRoute("HomePage"); - var cart = customer.ShoppingCartItems.Where(sci => sci.ShoppingCartTypeId == ShoppingCartType.Wishlist); - - if (!string.IsNullOrEmpty(_contextAccessor.StoreContext.CurrentStore.Id)) - cart = cart.LimitPerStore(_shoppingCartSettings.SharedCartBetweenStores, _contextAccessor.StoreContext.CurrentStore.Id); + var cart = await _shoppingCartService.GetShoppingCart(customer, _contextAccessor.StoreContext.CurrentStore.Id, + ShoppingCartType.Wishlist); var model = await _mediator.Send(new GetWishlist { - Cart = cart.ToList(), + Cart = cart, Customer = customer, Language = _contextAccessor.WorkContext.WorkingLanguage, Currency = _contextAccessor.WorkContext.WorkingCurrency, @@ -119,7 +114,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode if (ModelState.IsValid) { var cart = - (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) + (await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.Wishlist)) .FirstOrDefault(x => x.Id == model.ShoppingCartId); if (cart != null) { @@ -141,7 +136,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode success = !warnings.Any(), warnings = string.Join(", ", warnings), totalproducts = - (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) + (await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.Wishlist)) .Sum(x => x.Quantity) }); } @@ -216,7 +211,7 @@ public virtual async Task EmailWishlist(WishlistEmailAFriendModel !_shoppingCartSettings.EmailWishlistEnabled) return Content(""); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist); + var cart = await _shoppingCartService.GetCurrentCart(_contextAccessor, ShoppingCartType.Wishlist); if (!cart.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Features/Handlers/Customers/GetInfoHandler.cs b/src/Web/Grand.Web/Features/Handlers/Customers/GetInfoHandler.cs index 10206f96e7..67ad687dae 100644 --- a/src/Web/Grand.Web/Features/Handlers/Customers/GetInfoHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Customers/GetInfoHandler.cs @@ -209,7 +209,8 @@ private async Task PrepareExternalAuth(CustomerInfoModel model, GetInfo request) .LoadActiveAuthenticationProviders(request.Customer, request.Store).Count; foreach (var ear in await _externalAuthenticationService.GetExternalIdentifiers(request.Customer)) { - if (!_externalAuthenticationService.AuthenticationProviderIsAvailable(ear.ProviderSystemName)) + if (!_externalAuthenticationService.AuthenticationProviderIsAvailable(ear.ProviderSystemName, + request.Customer, request.Store)) continue; var authMethod = diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index 3d1799d183..82712141ea 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -22,7 +22,6 @@ using Grand.Domain.Orders; using Grand.Domain.Tax; using Grand.Infrastructure.Configuration; -using Grand.Infrastructure; using Grand.Web.Common.Extensions; using Grand.Web.Common.Localization; using Grand.Web.Extensions; @@ -36,7 +35,6 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetShoppingCartHandler : IRequestHandler { - private readonly IContextAccessor _contextAccessor; private readonly CatalogSettings _catalogSettings; private readonly ICheckoutAttributeFormatter _checkoutAttributeFormatter; private readonly ICheckoutAttributeParser _checkoutAttributeParser; @@ -95,10 +93,8 @@ public GetShoppingCartHandler( ShoppingCartSettings shoppingCartSettings, CatalogSettings catalogSettings, CommonSettings commonSettings, - IEnumTranslationService enumTranslationService, - IContextAccessor contextAccessor) + IEnumTranslationService enumTranslationService) { - _contextAccessor = contextAccessor; _productService = productService; _pictureService = pictureService; _translationService = translationService; @@ -132,6 +128,10 @@ public GetShoppingCartHandler( public async Task Handle(GetShoppingCart request, CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(request.Customer); + ArgumentNullException.ThrowIfNull(request.Store); + ArgumentNullException.ThrowIfNull(request.Host); + var model = new ShoppingCartModel(); if (!request.Cart.Any()) @@ -165,7 +165,7 @@ private async Task PrepareSimpleProperties(ShoppingCartModel model, GetShoppingC request.Store.Id); model.CheckoutAttributeInfo = await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, CalculationContextOf(request), - request.Language, _contextAccessor.StoreContext.CurrentHost); + request.Language, request.Host); if (!request.Cart.Where(x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions) .ToList().Any()) { diff --git a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetShoppingCart.cs b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetShoppingCart.cs index 6094ecc7e1..ce49bd7fce 100644 --- a/src/Web/Grand.Web/Features/Models/ShoppingCart/GetShoppingCart.cs +++ b/src/Web/Grand.Web/Features/Models/ShoppingCart/GetShoppingCart.cs @@ -15,6 +15,12 @@ public class GetShoppingCart : IRequest public Language Language { get; set; } public Currency Currency { get; set; } public Domain.Stores.Store Store { get; set; } + + /// + /// Store address of the request, used for the download links of checkout attributes + /// + public DomainHost Host { get; set; } + public TaxDisplayType TaxDisplayType { get; set; } public IList Cart { get; set; } public bool IsEditable { get; set; } = true; diff --git a/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs b/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs index 8fc61cc9f3..1f62d25a34 100644 --- a/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs +++ b/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs @@ -6,6 +6,7 @@ using Grand.Infrastructure; using Grand.Infrastructure.Validators; using Grand.Web.Models.ShoppingCart; +using Grand.Web.Common.Extensions; namespace Grand.Web.Validators.ShoppingCart; @@ -20,25 +21,20 @@ public UpdateQuantityValidator( RuleFor(x => x.Quantity).GreaterThan(0).WithMessage("Wrong quantity"); RuleFor(x => x).CustomAsync(async (x, context, _) => { - switch (x.ShoppingCartType) + var cart = (await shoppingCartService.GetCurrentCart(contextAccessor, PrepareCartTypes())) + .FirstOrDefault(z => z.Id == x.ShoppingCartId); + if (cart == null) { - case ShoppingCartType.ShoppingCart: - { - if (!await permissionService.Authorize(StandardPermission.EnableShoppingCart)) - context.AddFailure("No permission"); - break; - } - case ShoppingCartType.Wishlist: - { - if (!await permissionService.Authorize(StandardPermission.EnableWishlist)) - context.AddFailure("No permission"); - break; - } + context.AddFailure("Shopping cart item not found"); + return; } - var cart = (await shoppingCartService.GetShoppingCart(contextAccessor.WorkContext.CurrentCustomer, contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes())) - .FirstOrDefault(z => z.Id == x.ShoppingCartId); - if (cart == null) context.AddFailure("Shopping cart item not found"); + //check the permission of the item's own type; the type sent by the client is not trusted + var permission = cart.ShoppingCartTypeId == ShoppingCartType.Wishlist + ? StandardPermission.EnableWishlist + : StandardPermission.EnableShoppingCart; + if (!await permissionService.Authorize(permission)) + context.AddFailure("No permission"); }); ShoppingCartType[] PrepareCartTypes() diff --git a/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml b/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml index 68e2824aba..54b99572b6 100644 --- a/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml +++ b/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml @@ -19,17 +19,12 @@ var wishlistEnabled = await permissionService.Authorize(StandardPermission.EnableWishlist); var miniShoppingCartEnabled = shoppingCartSettings.MiniShoppingCartEnabled; - var shoppingCartItems = - contextAccessor.WorkContext.CurrentCustomer.ShoppingCartItems.Any() - ? (await shoppingCartService.GetShoppingCart(contextAccessor.WorkContext.CurrentCustomer, contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) - .Sum(x => x.Quantity) - : 0; + var cart = contextAccessor.WorkContext.CurrentCustomer.ShoppingCartItems.Any() + ? await shoppingCartService.GetCurrentCart(contextAccessor, [.. shoppingCartTypes, ShoppingCartType.Wishlist]) + : []; - - var wishlistItems = contextAccessor.WorkContext.CurrentCustomer.ShoppingCartItems.Any() - ? (await shoppingCartService.GetShoppingCart(contextAccessor.WorkContext.CurrentCustomer, contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) - .Sum(x => x.Quantity) - : 0; + var shoppingCartItems = cart.Where(x => shoppingCartTypes.Contains(x.ShoppingCartTypeId)).Sum(x => x.Quantity); + var wishlistItems = cart.Where(x => x.ShoppingCartTypeId == ShoppingCartType.Wishlist).Sum(x => x.Quantity); } @if (catalogSettings.CompareProductsEnabled) {