diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 8b2e87a8..76389888 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -53,6 +53,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 25558f9f..865f6f55 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -14,6 +14,7 @@ @inject IStringLocalizer Localizer @inject INotificationService NotificationService @inject IStringLocalizer SharedLocalizer +@inject ICacheService CacheService @if (_initialized) { @@ -50,11 +51,12 @@
- +
@Localizer["Browse"] +
@@ -732,7 +734,7 @@ settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true); settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true); settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true); - + //cookie consent settings = SettingService.SetSetting(settings, "CookieConsent", _cookieconsent); @@ -932,4 +934,9 @@ _aliasname = ""; StateHasChanged(); } + + private async Task EvictSitemapOutputCache() { + await CacheService.EvictOutputCacheByTag(Constants.SitemapOutputCacheTag); + AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success); + } } diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index d82b484c..37428819 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -349,7 +349,7 @@ Relay Configured? - The site map url for this site which can be submitted to search engines for indexing + The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared. Site Map: @@ -441,4 +441,10 @@ Theme + + Clear Cache + + + SiteMap Output Cache Evicted + \ No newline at end of file diff --git a/Oqtane.Client/Services/CacheService.cs b/Oqtane.Client/Services/CacheService.cs new file mode 100644 index 00000000..4856b1e9 --- /dev/null +++ b/Oqtane.Client/Services/CacheService.cs @@ -0,0 +1,23 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Oqtane.Documentation; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + /// + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class CacheService : ServiceBase, ICacheService + { + public CacheService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string ApiUrl => CreateApiUrl("Cache"); + + public async Task EvictOutputCacheByTag(string tag, CancellationToken cancellationToken = default) + { + await DeleteAsync($"{ApiUrl}/outputCache/evictByTag/{tag}"); + } + } +} diff --git a/Oqtane.Client/Services/Interfaces/ICacheService.cs b/Oqtane.Client/Services/Interfaces/ICacheService.cs new file mode 100644 index 00000000..910cde25 --- /dev/null +++ b/Oqtane.Client/Services/Interfaces/ICacheService.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Oqtane.Services +{ + /// + /// Service to manage cache + /// + public interface ICacheService + { + /// + /// Evicts the output cache for a specific tag + /// + /// + /// + Task EvictOutputCacheByTag(string tag, CancellationToken cancellationToken = default); + } +} diff --git a/Oqtane.Server/Controllers/CacheController.cs b/Oqtane.Server/Controllers/CacheController.cs new file mode 100644 index 00000000..e2d0ed08 --- /dev/null +++ b/Oqtane.Server/Controllers/CacheController.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +using Oqtane.Models; +using Oqtane.Services; +using Oqtane.Shared; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class CacheController : Controller + { + private readonly ICacheService _cacheService; + + public CacheController(ICacheService cacheService) + { + _cacheService = cacheService; + } + + // DELETE api//outputCache/evictByTag/{tag} + [HttpDelete("outputCache/evictByTag/{tag}")] + [Authorize(Roles = RoleNames.Admin)] + public async Task EvictOutputCacheByTag(string tag, CancellationToken cancellationToken = default) + { + await _cacheService.EvictOutputCacheByTag(tag, cancellationToken); + } + } +} diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 52bb3d3e..bf34816d 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -117,6 +117,7 @@ namespace Microsoft.Extensions.DependencyInjection // services services.AddTransient(); services.AddTransient(); + services.AddTransient(); // repositories services.AddTransient(); diff --git a/Oqtane.Server/Pages/Sitemap.cshtml.cs b/Oqtane.Server/Pages/Sitemap.cshtml.cs index 96501294..c564fb3c 100644 --- a/Oqtane.Server/Pages/Sitemap.cshtml.cs +++ b/Oqtane.Server/Pages/Sitemap.cshtml.cs @@ -7,6 +7,7 @@ using System.Xml; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.OutputCaching; using Microsoft.Extensions.DependencyInjection; using Oqtane.Enums; using Oqtane.Infrastructure; @@ -19,6 +20,7 @@ using Oqtane.Shared; namespace Oqtane.Pages { [AllowAnonymous] + [OutputCache(Duration = 300, Tags = [Constants.SitemapOutputCacheTag])] public class SitemapModel : PageModel { private readonly IServiceProvider _serviceProvider; diff --git a/Oqtane.Server/Services/CacheService.cs b/Oqtane.Server/Services/CacheService.cs new file mode 100644 index 00000000..ed54d547 --- /dev/null +++ b/Oqtane.Server/Services/CacheService.cs @@ -0,0 +1,41 @@ +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OutputCaching; + +using Oqtane.Documentation; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class ServerCacheService : ICacheService + { + private readonly IOutputCacheStore _outputCacheStore; + private readonly ILogManager _logger; + private readonly IHttpContextAccessor _accessor; + + public ServerCacheService(IOutputCacheStore outputCacheStore, ILogManager logger, IHttpContextAccessor accessor) + { + _outputCacheStore = outputCacheStore; + _logger = logger; + _accessor = accessor; + } + + public async Task EvictOutputCacheByTag(string tag, CancellationToken cancellationToken = default) + { + if (_accessor.HttpContext.User.IsInRole(RoleNames.Admin)) + { + await _outputCacheStore.EvictByTagAsync(tag, cancellationToken); + _logger.Log(LogLevel.Information, this, LogFunction.Other, "Evicted Output Cache for Tag {Tag}", tag); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Output Cache Eviction for {Tag}", tag); + } + } + } +} diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 8ab42dd8..319d1471 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -142,6 +142,8 @@ namespace Oqtane }); }); + services.AddOutputCache(); + services.AddMvc(options => { options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); @@ -222,6 +224,7 @@ namespace Oqtane app.UseJwtAuthorization(); app.UseRouting(); app.UseCors(); + app.UseOutputCache(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 7e99421a..5e7b2e0e 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -94,6 +94,8 @@ namespace Oqtane.Shared public const string CookieConsentCookieName = "Oqtane.CookieConsent"; public const string CookieConsentCookieValue = "yes"; public const string CookieConsentActionCookieValue = "yes"; + + public const string SitemapOutputCacheTag = "Sitemap"; // Obsolete constants const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames";