From 74952cf62de92cfaba9e1dff7f86fa50a810e9b3 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 5 Mar 2024 08:44:09 -0500 Subject: [PATCH] implement client and server service implementations in Html/Text module --- .../HtmlText/Services/HtmlTextService.cs | 7 +- .../HtmlText/Services/IHtmlTextService.cs | 2 +- Oqtane.Client/Program.cs | 10 ++ .../OqtaneServiceCollectionExtensions.cs | 20 +++- .../Controllers/HtmlTextController.cs | 36 +++--- .../HtmlText/Repository/HtmlTextRepository.cs | 32 ++++++ .../Repository/IHtmlTextRepository.cs | 7 +- .../HtmlText/Services/HtmlTextService.cs | 108 ++++++++++++++++++ Oqtane.Server/Repository/SiteRepository.cs | 4 +- Oqtane.Shared/Interfaces/IClientService.cs | 9 ++ Oqtane.Shared/Interfaces/IServerService.cs | 9 ++ 11 files changed, 214 insertions(+), 30 deletions(-) create mode 100644 Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs create mode 100644 Oqtane.Shared/Interfaces/IClientService.cs create mode 100644 Oqtane.Shared/Interfaces/IServerService.cs diff --git a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs index e4dd0174..f2f0674c 100644 --- a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs +++ b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Oqtane.Documentation; @@ -9,7 +8,7 @@ using Oqtane.Shared; namespace Oqtane.Modules.HtmlText.Services { [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] - public class HtmlTextService : ServiceBase, IHtmlTextService, IService + public class HtmlTextService : ServiceBase, IHtmlTextService, IClientService { public HtmlTextService(HttpClient http, SiteState siteState) : base(http, siteState) {} @@ -30,9 +29,9 @@ namespace Oqtane.Modules.HtmlText.Services return await GetJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlTextId}/{moduleId}", EntityNames.Module, moduleId)); } - public async Task AddHtmlTextAsync(Models.HtmlText htmlText) + public async Task AddHtmlTextAsync(Models.HtmlText htmlText) { - await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText); + return await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText); } public async Task DeleteHtmlTextAsync(int htmlTextId, int moduleId) diff --git a/Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs b/Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs index 7ca069c6..838d5240 100644 --- a/Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs +++ b/Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs @@ -13,7 +13,7 @@ namespace Oqtane.Modules.HtmlText.Services Task GetHtmlTextAsync(int htmlTextId, int moduleId); - Task AddHtmlTextAsync(Models.HtmlText htmltext); + Task AddHtmlTextAsync(Models.HtmlText htmltext); Task DeleteHtmlTextAsync(int htmlTextId, int moduleId); } diff --git a/Oqtane.Client/Program.cs b/Oqtane.Client/Program.cs index dad17d44..9f5ed2ff 100644 --- a/Oqtane.Client/Program.cs +++ b/Oqtane.Client/Program.cs @@ -220,6 +220,16 @@ namespace Oqtane.Client services.AddScoped(serviceType ?? implementationType, implementationType); } } + + implementationTypes = assembly.GetInterfaces(); + foreach (var implementationType in implementationTypes) + { + if (implementationType.AssemblyQualifiedName != null) + { + var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); + services.AddScoped(serviceType ?? implementationType, implementationType); + } + } } catch { diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index a3324e49..14f97dff 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -292,11 +292,29 @@ namespace Microsoft.Extensions.DependencyInjection { if (implementationType.AssemblyQualifiedName != null) { - var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); + var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); var serviceName = implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}"); services.AddScoped(serviceType ?? implementationType, implementationType); } } + // dynamically register module server scoped services (using conventions) + implementationTypes = assembly.GetInterfaces(); + foreach (var implementationType in implementationTypes) + { + if (implementationType.AssemblyQualifiedName != null && implementationType.AssemblyQualifiedName.Contains("Services.Server")) + { + // module server services reference a common interface which is located in the client assembly + var serviceName = implementationType.AssemblyQualifiedName + // convert implementation type name to interface name and change Server assembly to Client + .Replace(".Services.Server", ".Services.I").Replace(".Server,", ".Client,"); + var serviceType = Type.GetType(serviceName); + if (serviceType != null) + { + services.AddScoped(serviceType, implementationType); + } + } + } + // dynamically register module transient services (ie. server DBContext, repository classes) implementationTypes = assembly.GetInterfaces(); foreach (var implementationType in implementationTypes) diff --git a/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs b/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs index e6d64241..85fe9f5b 100644 --- a/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs +++ b/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs @@ -10,6 +10,8 @@ using System.Net; using Oqtane.Documentation; using System.Collections.Generic; using System.Linq; +using Oqtane.Modules.HtmlText.Services; +using System.Threading.Tasks; namespace Oqtane.Modules.HtmlText.Controllers { @@ -17,21 +19,21 @@ namespace Oqtane.Modules.HtmlText.Controllers [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] public class HtmlTextController : ModuleControllerBase { - private readonly IHtmlTextRepository _htmlText; + private readonly IHtmlTextService _htmlTextService; - public HtmlTextController(IHtmlTextRepository htmlText, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor) + public HtmlTextController(IHtmlTextService htmlTextService, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor) { - _htmlText = htmlText; + _htmlTextService = htmlTextService; } // GET: api/?moduleid=x [HttpGet] [Authorize(Roles = RoleNames.Registered)] - public IEnumerable Get(string moduleId) + public async Task> Get(string moduleId) { if (int.TryParse(moduleId, out int ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId)) { - return _htmlText.GetHtmlTexts(ModuleId); + return await _htmlTextService.GetHtmlTextsAsync(ModuleId); } else { @@ -44,19 +46,11 @@ namespace Oqtane.Modules.HtmlText.Controllers // GET api//5 [HttpGet("{moduleId}")] [Authorize(Policy = PolicyNames.ViewModule)] - public Models.HtmlText Get(int moduleId) + public async Task Get(int moduleId) { if (IsAuthorizedEntityId(EntityNames.Module, moduleId)) { - var htmltexts = _htmlText.GetHtmlTexts(moduleId); - if (htmltexts != null && htmltexts.Any()) - { - return htmltexts.OrderByDescending(item => item.CreatedOn).First(); - } - else - { - return null; - } + return await _htmlTextService.GetHtmlTextAsync(moduleId); } else { @@ -69,11 +63,11 @@ namespace Oqtane.Modules.HtmlText.Controllers // GET api//5/6 [HttpGet("{id}/{moduleId}")] [Authorize(Policy = PolicyNames.ViewModule)] - public Models.HtmlText Get(int id, int moduleId) + public async Task Get(int id, int moduleId) { if (IsAuthorizedEntityId(EntityNames.Module, moduleId)) { - return _htmlText.GetHtmlText(id); + return await _htmlTextService.GetHtmlTextAsync(id, moduleId); } else { @@ -86,11 +80,11 @@ namespace Oqtane.Modules.HtmlText.Controllers // POST api/ [HttpPost] [Authorize(Policy = PolicyNames.EditModule)] - public Models.HtmlText Post([FromBody] Models.HtmlText htmlText) + public async Task Post([FromBody] Models.HtmlText htmlText) { if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, htmlText.ModuleId)) { - htmlText = _htmlText.AddHtmlText(htmlText); + htmlText = await _htmlTextService.AddHtmlTextAsync(htmlText); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Html/Text Added {HtmlText}", htmlText); return htmlText; } @@ -105,11 +99,11 @@ namespace Oqtane.Modules.HtmlText.Controllers // DELETE api//5 [HttpDelete("{id}/{moduleId}")] [Authorize(Policy = PolicyNames.EditModule)] - public void Delete(int id, int moduleId) + public async Task Delete(int id, int moduleId) { if (IsAuthorizedEntityId(EntityNames.Module, moduleId)) { - _htmlText.DeleteHtmlText(id); + await _htmlTextService.DeleteHtmlTextAsync(id, moduleId); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Html/Text Deleted {HtmlTextId}", id); } else diff --git a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs index 21527009..21e0ffeb 100644 --- a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs +++ b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using Microsoft.Extensions.Caching.Memory; using Oqtane.Infrastructure; using System; +using Microsoft.EntityFrameworkCore; +using System.Threading.Tasks; namespace Oqtane.Modules.HtmlText.Repository { @@ -51,6 +53,36 @@ namespace Oqtane.Modules.HtmlText.Repository _db.SaveChanges(); } + public async Task> GetHtmlTextsAsync(int moduleId) + { + return await _cache.GetOrCreateAsync($"HtmlText:{_siteState.Alias.SiteKey}:{moduleId}", async entry => + { + entry.SlidingExpiration = TimeSpan.FromMinutes(30); + return await _db.HtmlText.Where(item => item.ModuleId == moduleId).ToListAsync(); + }); + } + + public async Task GetHtmlTextAsync(int htmlTextId) + { + return await _db.HtmlText.FindAsync(htmlTextId); + } + + public async Task AddHtmlTextAsync(Models.HtmlText htmlText) + { + _db.HtmlText.Add(htmlText); + await _db.SaveChangesAsync(); + ClearCache(htmlText.ModuleId); + return htmlText; + } + + public async Task DeleteHtmlTextAsync(int htmlTextId) + { + Models.HtmlText htmlText = _db.HtmlText.FirstOrDefault(item => item.HtmlTextId == htmlTextId); + _db.HtmlText.Remove(htmlText); + ClearCache(htmlText.ModuleId); + await _db.SaveChangesAsync(); + } + private void ClearCache(int moduleId) { _cache.Remove($"HtmlText:{_siteState.Alias.SiteKey}:{moduleId}"); diff --git a/Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs b/Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs index 44be5973..8715ccc0 100644 --- a/Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs +++ b/Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Oqtane.Documentation; -using Oqtane.Modules.HtmlText.Models; namespace Oqtane.Modules.HtmlText.Repository { @@ -11,5 +11,10 @@ namespace Oqtane.Modules.HtmlText.Repository Models.HtmlText GetHtmlText(int htmlTextId); Models.HtmlText AddHtmlText(Models.HtmlText htmlText); void DeleteHtmlText(int htmlTextId); + + Task> GetHtmlTextsAsync(int moduleId); + Task GetHtmlTextAsync(int htmlTextId); + Task AddHtmlTextAsync(Models.HtmlText htmlText); + Task DeleteHtmlTextAsync(int htmlTextId); } } diff --git a/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs b/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs new file mode 100644 index 00000000..9cbf4ed8 --- /dev/null +++ b/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Oqtane.Documentation; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Modules.HtmlText.Repository; +using Oqtane.Security; +using Oqtane.Shared; + +namespace Oqtane.Modules.HtmlText.Services +{ + [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] + public class ServerHtmlTextService : IHtmlTextService, IServerService + { + private readonly IHtmlTextRepository _htmlText; + private readonly IUserPermissions _userPermissions; + private readonly ITenantManager _tenantManager; + private readonly ILogManager _logger; + private readonly IHttpContextAccessor _accessor; + + public ServerHtmlTextService(IHtmlTextRepository htmlText, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor) + { + _htmlText = htmlText; + _userPermissions = userPermissions; + _tenantManager = tenantManager; + _logger = logger; + _accessor = accessor; + } + + public async Task> GetHtmlTextsAsync(int moduleId) + { + if (_accessor.HttpContext.User.IsInRole(RoleNames.Registered)) + { + return (await _htmlText.GetHtmlTextsAsync(moduleId)).ToList(); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Get Attempt {ModuleId}", moduleId); + return null; + } + } + + public async Task GetHtmlTextAsync(int moduleId) + { + var alias = _tenantManager.GetAlias(); + if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, alias.SiteId, EntityNames.Module, moduleId, PermissionNames.View)) + { + var htmltexts = await _htmlText.GetHtmlTextsAsync(moduleId); + if (htmltexts != null && htmltexts.Any()) + { + return htmltexts.OrderByDescending(item => item.CreatedOn).First(); + } + else + { + return null; + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Get Attempt {ModuleId}", moduleId); + return null; + } + } + + public async Task GetHtmlTextAsync(int htmlTextId, int moduleId) + { + var alias = _tenantManager.GetAlias(); + if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, alias.SiteId, EntityNames.Module, moduleId, PermissionNames.View)) + { + return await _htmlText.GetHtmlTextAsync(htmlTextId); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Get Attempt {HtmlTextId} {ModuleId}", htmlTextId, moduleId); + return null; + } + } + + public async Task AddHtmlTextAsync(Models.HtmlText htmlText) + { + var alias = _tenantManager.GetAlias(); + if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, alias.SiteId, EntityNames.Module, htmlText.ModuleId, PermissionNames.Edit)) + { + return await _htmlText.AddHtmlTextAsync(htmlText); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Add Attempt {HtmlText}", htmlText); + return null; + } + } + + public async Task DeleteHtmlTextAsync(int htmlTextId, int moduleId) + { + var alias = _tenantManager.GetAlias(); + if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, alias.SiteId, EntityNames.Module, moduleId, PermissionNames.Edit)) + { + await _htmlText.DeleteHtmlTextAsync(htmlTextId); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Delete Attempt {HtmlTextId} {ModuleId}", htmlTextId, moduleId); + } + } + } +} diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index 8fef197a..a37b70e6 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -92,9 +92,9 @@ namespace Oqtane.Repository public async Task DeleteSiteAsync(int siteId) { - var site = await _db.Site.FindAsync(siteId); + var site = _db.Site.Find(siteId); _db.Site.Remove(site); - _db.SaveChanges(); + await _db.SaveChangesAsync(); } // synchronous methods diff --git a/Oqtane.Shared/Interfaces/IClientService.cs b/Oqtane.Shared/Interfaces/IClientService.cs new file mode 100644 index 00000000..ad1bebb7 --- /dev/null +++ b/Oqtane.Shared/Interfaces/IClientService.cs @@ -0,0 +1,9 @@ +namespace Oqtane.Modules +{ + /// + /// Empty interface used to decorate client module services for auto registration as scoped + /// + public interface IClientService + { + } +} diff --git a/Oqtane.Shared/Interfaces/IServerService.cs b/Oqtane.Shared/Interfaces/IServerService.cs new file mode 100644 index 00000000..d5d888fd --- /dev/null +++ b/Oqtane.Shared/Interfaces/IServerService.cs @@ -0,0 +1,9 @@ +namespace Oqtane.Modules +{ + /// + /// Empty interface used to decorate server module services for auto registration as scoped + /// + public interface IServerService + { + } +}