diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/IEmailAccountService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/IEmailAccountService.cs index 10b3f2f8c..3c9efc374 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Messages/IEmailAccountService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/IEmailAccountService.cs @@ -34,4 +34,11 @@ public interface IEmailAccountService /// /// Email accounts list Task> GetAllEmailAccounts(); + + /// + /// Gets email accounts belonging to the specified store. + /// + /// Store identifier + /// Email accounts for the given store + Task> GetEmailAccountsByStore(string storeId); } \ No newline at end of file diff --git a/src/Business/Grand.Business.Messages/Services/EmailAccountService.cs b/src/Business/Grand.Business.Messages/Services/EmailAccountService.cs index af52922b0..06f8c2f9a 100644 --- a/src/Business/Grand.Business.Messages/Services/EmailAccountService.cs +++ b/src/Business/Grand.Business.Messages/Services/EmailAccountService.cs @@ -146,4 +146,18 @@ public virtual async Task> GetAllEmailAccounts() return await Task.FromResult(query.ToList()); }); } + + /// + /// Gets email accounts belonging to the specified store. + /// + /// Store identifier + /// Email accounts for the given store + public virtual async Task> GetEmailAccountsByStore(string storeId) + { + var allAccounts = await GetAllEmailAccounts(); + if (string.IsNullOrEmpty(storeId)) + return allAccounts; + + return allAccounts.Where(ea => ea.StoreId == storeId).ToList(); + } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs index b72605454..e2e85bd53 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs @@ -93,13 +93,14 @@ protected virtual async Task GetMessageTemplate(string messageT } protected virtual async Task GetEmailAccountOfMessageTemplate(MessageTemplate messageTemplate, - string languageId) + string languageId, string storeId = "") { var emailAccounId = messageTemplate.GetTranslation(mt => mt.EmailAccountId, languageId); - var emailAccount = (await _emailAccountService.GetEmailAccountById(emailAccounId) ?? - await _emailAccountService.GetEmailAccountById(_emailAccountSettings - .DefaultEmailAccountId)) ?? - (await _emailAccountService.GetAllEmailAccounts()).FirstOrDefault(); + var emailAccount = await _emailAccountService.GetEmailAccountById(emailAccounId); + emailAccount ??= await _emailAccountService.GetEmailAccountById(_emailAccountSettings.DefaultEmailAccountId); + if (emailAccount == null && !string.IsNullOrEmpty(storeId)) + emailAccount = (await _emailAccountService.GetEmailAccountsByStore(storeId)).FirstOrDefault(); + emailAccount ??= (await _emailAccountService.GetAllEmailAccounts()).FirstOrDefault(); return emailAccount; } diff --git a/src/Core/Grand.Domain/Messages/EmailAccount.cs b/src/Core/Grand.Domain/Messages/EmailAccount.cs index dfbe59f38..4eee08453 100644 --- a/src/Core/Grand.Domain/Messages/EmailAccount.cs +++ b/src/Core/Grand.Domain/Messages/EmailAccount.cs @@ -45,4 +45,10 @@ public class EmailAccount : BaseEntity /// Provides a way of specifying the SSL and/or TLS encryption that should be used for a connection /// public int SecureSocketOptionsId { get; set; } + + /// + /// Gets or sets the store identifier this email account belongs to. + /// An empty string means this is a global/shared email account. + /// + public string StoreId { get; set; } } \ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/EmailAccount/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/EmailAccount/Partials/CreateOrUpdate.cshtml index aa1a85a72..66ca5ee47 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/EmailAccount/Partials/CreateOrUpdate.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/EmailAccount/Partials/CreateOrUpdate.cshtml @@ -68,6 +68,12 @@ + + + + + + @if (!string.IsNullOrEmpty(Model.Id)) { diff --git a/src/Web/Grand.Web.Admin/Controllers/EmailAccountController.cs b/src/Web/Grand.Web.Admin/Controllers/EmailAccountController.cs index e93b33183..541237ff7 100644 --- a/src/Web/Grand.Web.Admin/Controllers/EmailAccountController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/EmailAccountController.cs @@ -78,9 +78,9 @@ public async Task MarkAsDefaultEmail(string id) } [PermissionAuthorizeAction(PermissionActionName.Create)] - public IActionResult Create() + public async Task Create() { - var model = _emailAccountViewModelService.PrepareEmailAccountModel(); + var model = await _emailAccountViewModelService.PrepareEmailAccountModel(); return View(model); } @@ -108,7 +108,9 @@ public async Task Edit(string id) //No email account found with the specified id return RedirectToAction("List"); - return View(emailAccount.ToModel()); + var model = emailAccount.ToModel(); + await _emailAccountViewModelService.PrepareAvailableStores(model); + return View(model); } [HttpPost] diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IEmailAccountViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IEmailAccountViewModelService.cs index fb5df4b5f..e74daae03 100644 --- a/src/Web/Grand.Web.AdminShared/Interfaces/IEmailAccountViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IEmailAccountViewModelService.cs @@ -5,7 +5,8 @@ namespace Grand.Web.AdminShared.Interfaces; public interface IEmailAccountViewModelService { - EmailAccountModel PrepareEmailAccountModel(); + Task PrepareEmailAccountModel(); + Task PrepareAvailableStores(EmailAccountModel model); Task InsertEmailAccountModel(EmailAccountModel model); Task UpdateEmailAccountModel(EmailAccount emailAccount, EmailAccountModel model); Task SendTestEmail(EmailAccount emailAccount, EmailAccountModel model); diff --git a/src/Web/Grand.Web.AdminShared/Mapper/EmailAccountProfile.cs b/src/Web/Grand.Web.AdminShared/Mapper/EmailAccountProfile.cs index f0e257525..63548d254 100644 --- a/src/Web/Grand.Web.AdminShared/Mapper/EmailAccountProfile.cs +++ b/src/Web/Grand.Web.AdminShared/Mapper/EmailAccountProfile.cs @@ -12,7 +12,8 @@ public EmailAccountProfile() CreateMap() .ForMember(dest => dest.Password, mo => mo.Ignore()) .ForMember(dest => dest.IsDefaultEmailAccount, mo => mo.Ignore()) - .ForMember(dest => dest.SendTestEmailTo, mo => mo.Ignore()); + .ForMember(dest => dest.SendTestEmailTo, mo => mo.Ignore()) + .ForMember(dest => dest.AvailableStores, mo => mo.Ignore()); CreateMap() .ForMember(dest => dest.Id, mo => mo.Ignore()) diff --git a/src/Web/Grand.Web.AdminShared/Models/Messages/EmailAccountModel.cs b/src/Web/Grand.Web.AdminShared/Models/Messages/EmailAccountModel.cs index c0730a9a5..469cbf4b1 100644 --- a/src/Web/Grand.Web.AdminShared/Models/Messages/EmailAccountModel.cs +++ b/src/Web/Grand.Web.AdminShared/Models/Messages/EmailAccountModel.cs @@ -1,5 +1,6 @@ using Grand.Infrastructure.ModelBinding; using Grand.Infrastructure.Models; +using Microsoft.AspNetCore.Mvc.Rendering; namespace Grand.Web.AdminShared.Models.Messages; @@ -34,4 +35,9 @@ public class EmailAccountModel : BaseEntityModel [GrandResourceDisplayName("Admin.Configuration.EmailAccounts.Fields.SendTestEmailTo")] public string SendTestEmailTo { get; set; } + + [GrandResourceDisplayName("Admin.Configuration.EmailAccounts.Fields.Store")] + public string StoreId { get; set; } + + public IList AvailableStores { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Web/Grand.Web.AdminShared/Services/EmailAccountViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/EmailAccountViewModelService.cs index e228ad2c5..837f7522b 100644 --- a/src/Web/Grand.Web.AdminShared/Services/EmailAccountViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/EmailAccountViewModelService.cs @@ -1,8 +1,11 @@ -using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Messages; using Grand.Domain.Messages; using Grand.Web.AdminShared.Extensions.Mapping; using Grand.Web.AdminShared.Interfaces; using Grand.Web.AdminShared.Models.Messages; +using Microsoft.AspNetCore.Mvc.Rendering; namespace Grand.Web.AdminShared.Services; @@ -10,22 +13,33 @@ public class EmailAccountViewModelService : IEmailAccountViewModelService { private readonly IEmailAccountService _emailAccountService; private readonly IEmailSender _emailSender; + private readonly IStoreService _storeService; + private readonly ITranslationService _translationService; - public EmailAccountViewModelService(IEmailAccountService emailAccountService, IEmailSender emailSender) + public EmailAccountViewModelService(IEmailAccountService emailAccountService, IEmailSender emailSender, + IStoreService storeService, ITranslationService translationService) { _emailAccountService = emailAccountService; _emailSender = emailSender; + _storeService = storeService; + _translationService = translationService; } - public virtual EmailAccountModel PrepareEmailAccountModel() + public virtual async Task PrepareEmailAccountModel() { var model = new EmailAccountModel { //default values Port = 25 }; + await PopulateAvailableStores(model); return model; } + public virtual async Task PrepareAvailableStores(EmailAccountModel model) + { + await PopulateAvailableStores(model); + } + public virtual async Task InsertEmailAccountModel(EmailAccountModel model) { var emailAccount = model.ToEntity(); @@ -52,4 +66,17 @@ public virtual async Task SendTestEmail(EmailAccount emailAccount, EmailAccountM await _emailSender.SendEmail(emailAccount, subject, body, emailAccount.Email, emailAccount.DisplayName, model.SendTestEmailTo, null); } + + private async Task PopulateAvailableStores(EmailAccountModel model) + { + model.AvailableStores.Add(new SelectListItem { + Value = "", + Text = _translationService.GetResource("Admin.Settings.StoreScope.AllStores") + }); + foreach (var store in await _storeService.GetAllStores()) + model.AvailableStores.Add(new SelectListItem { + Value = store.Id, + Text = store.Name + }); + } } \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/Create.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/Create.cshtml new file mode 100644 index 000000000..637d25174 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/Create.cshtml @@ -0,0 +1,34 @@ +@model EmailAccountModel +@{ + //page title + ViewBag.Title = Loc["Admin.Configuration.EmailAccounts.AddNew"]; +} + + + + + + + + + @Loc["Admin.Configuration.EmailAccounts.AddNew"] + + @Html.ActionLink(Loc["Admin.Configuration.EmailAccounts.BackToList"], "List") + + + + + @Loc["Admin.Common.Save"] + + + @Loc["Admin.Common.SaveContinue"] + + + + + + + + + + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/Edit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/Edit.cshtml new file mode 100644 index 000000000..f5bb6c9cf --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/Edit.cshtml @@ -0,0 +1,40 @@ +@model EmailAccountModel +@{ + //page title + ViewBag.Title = Loc["Admin.Configuration.EmailAccounts.EditEmailAccountDetails"]; +} + + + + + + + + + @Loc["Admin.Configuration.EmailAccounts.EditEmailAccountDetails"] + + @Html.ActionLink(Loc["Admin.Configuration.EmailAccounts.BackToList"], "List") + + + + + + @Loc["Admin.Common.Save"] + + + @Loc["Admin.Common.SaveContinue"] + + + @Loc["Admin.Common.Delete"] + + + + + + + + + + + + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/List.cshtml new file mode 100644 index 000000000..466531da8 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/List.cshtml @@ -0,0 +1,71 @@ +@{ + //page title + ViewBag.Title = Loc["Admin.Configuration.EmailAccounts"]; +} + + + + + + + + @Loc["Admin.Configuration.EmailAccounts"] + + + + @Loc["Admin.Common.AddNew"] + + + + + + + + + + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/Partials/CreateOrUpdate.cshtml new file mode 100644 index 000000000..eebb29bbd --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/Partials/CreateOrUpdate.cshtml @@ -0,0 +1,91 @@ +@using Grand.Business.Core.Enums.Messages +@model EmailAccountModel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @if (!string.IsNullOrEmpty(Model.Id)) + { + + + @Loc["Admin.Configuration.EmailAccounts.SendTestEmail"] + + + + + + + + + + + + + + } + + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml index 4d17948f3..bb0768c2b 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml @@ -31,6 +31,7 @@ @using Grand.Web.AdminShared.Models.Cms @using Grand.Web.AdminShared.Models.News @using Grand.Web.AdminShared.Models.Blogs +@using Grand.Web.AdminShared.Models.Messages @inject LocService Loc @inject IEnumTranslationService EnumTranslationService \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Controllers/EmailAccountController.cs b/src/Web/Grand.Web.Store/Controllers/EmailAccountController.cs new file mode 100644 index 000000000..987388169 --- /dev/null +++ b/src/Web/Grand.Web.Store/Controllers/EmailAccountController.cs @@ -0,0 +1,171 @@ +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Domain.Messages; +using Grand.Domain.Permissions; +using Grand.Infrastructure; +using Grand.SharedKernel; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Messages; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Filters; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Grand.Web.Store.Controllers; + +[PermissionAuthorize(PermissionSystemName.EmailAccounts)] +public class EmailAccountController( + IEmailAccountService emailAccountService, + IEmailAccountViewModelService emailAccountViewModelService, + ITranslationService translationService, + IContextAccessor contextAccessor) : BaseStoreController +{ + private string CurrentStoreId => contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + + public IActionResult List() + { + return View(); + } + + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.List)] + public async Task List(DataSourceRequest command) + { + var emailAccountModels = (await emailAccountService.GetEmailAccountsByStore(CurrentStoreId)) + .Select(x => x.ToModel()) + .ToList(); + + var gridModel = new DataSourceResult { + Data = emailAccountModels, + Total = emailAccountModels.Count + }; + + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Create)] + public IActionResult Create() + { + var model = new EmailAccountModel { + Port = 25, + StoreId = CurrentStoreId + }; + return View(model); + } + + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task Create(EmailAccountModel model, bool continueEditing) + { + // Force the account to belong to the current store + model.StoreId = CurrentStoreId; + + if (ModelState.IsValid) + { + var emailAccount = await emailAccountViewModelService.InsertEmailAccountModel(model); + Success(translationService.GetResource("Admin.Configuration.EmailAccounts.Added")); + return continueEditing + ? RedirectToAction("Edit", new { id = emailAccount.Id }) + : RedirectToAction("List"); + } + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task Edit(string id) + { + var emailAccount = await emailAccountService.GetEmailAccountById(id); + if (emailAccount == null || emailAccount.StoreId != CurrentStoreId) + return RedirectToAction("List"); + + var model = emailAccount.ToModel(); + return View(model); + } + + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task Edit(EmailAccountModel model, bool continueEditing) + { + var emailAccount = await emailAccountService.GetEmailAccountById(model.Id); + if (emailAccount == null || emailAccount.StoreId != CurrentStoreId) + return RedirectToAction("List"); + + // Prevent moving the account to another store + model.StoreId = CurrentStoreId; + + if (ModelState.IsValid) + { + emailAccount = await emailAccountViewModelService.UpdateEmailAccountModel(emailAccount, model); + Success(translationService.GetResource("Admin.Configuration.EmailAccounts.Updated")); + return continueEditing + ? RedirectToAction("Edit", new { id = emailAccount.Id }) + : RedirectToAction("List"); + } + + return View(model); + } + + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task SendTestEmail(EmailAccountModel model) + { + var emailAccount = await emailAccountService.GetEmailAccountById(model.Id); + if (emailAccount == null || emailAccount.StoreId != CurrentStoreId) + return RedirectToAction("List"); + + try + { + if (string.IsNullOrWhiteSpace(model.SendTestEmailTo)) + throw new GrandException(translationService.GetResource("Admin.Configuration.EmailAccounts.EnterTestEmail")); + if (ModelState.IsValid) + { + await emailAccountViewModelService.SendTestEmail(emailAccount, model); + Success(translationService.GetResource("Admin.Configuration.EmailAccounts.SendTestEmail.Success"), + false); + } + else + { + Error(ModelState); + } + } + catch (GrandException exc) + { + Error(exc.Message); + } + + return RedirectToAction("Edit", new { id = model.Id }); + } + + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.Delete)] + public async Task Delete(string id) + { + var emailAccount = await emailAccountService.GetEmailAccountById(id); + if (emailAccount == null || emailAccount.StoreId != CurrentStoreId) + return RedirectToAction("List"); + + try + { + if (ModelState.IsValid) + { + await emailAccountService.DeleteEmailAccount(emailAccount); + Success(translationService.GetResource("Admin.Configuration.EmailAccounts.Deleted")); + } + else + { + Error(ModelState); + } + + return RedirectToAction("List"); + } + catch (GrandException exc) + { + Error(exc); + return RedirectToAction("Edit", new { id = emailAccount.Id }); + } + } +} diff --git a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml index 3b37fcbdf..743f71b17 100644 Binary files a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml and b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml differ