From 982f3b1943f1a384b8c64345e76706cf394e1416 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 22 Feb 2025 09:49:33 +0800 Subject: [PATCH] Fix #4936: add the cookie consent theme control. --- .../OqtaneServiceCollectionExtensions.cs | 1 + .../Themes/Controls/CookieConsent.resx | 132 ++++++++++++++++++ .../Services/CookieConsentService.cs | 31 ++++ .../Interfaces/ICookieConsentService.cs | 24 ++++ .../Themes/BlazorTheme/Themes/Default.razor | 2 + .../Themes/Controls/Theme/CookieConsent.razor | 33 +++++ .../Themes/OqtaneTheme/Themes/Default.razor | 1 + Oqtane.Client/UI/Interop.cs | 13 ++ .../Controllers/CookieConsentController.cs | 34 +++++ .../OqtaneServiceCollectionExtensions.cs | 1 + .../Services/CookieConsentService.cs | 38 +++++ Oqtane.Server/Startup.cs | 8 ++ Oqtane.Server/wwwroot/js/interop.js | 3 + Oqtane.Shared/Shared/Constants.cs | 1 + 14 files changed, 322 insertions(+) create mode 100644 Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx create mode 100644 Oqtane.Client/Services/CookieConsentService.cs create mode 100644 Oqtane.Client/Services/Interfaces/ICookieConsentService.cs create mode 100644 Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor create mode 100644 Oqtane.Server/Controllers/CookieConsentController.cs create mode 100644 Oqtane.Server/Services/CookieConsentService.cs diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 457af666..8b2e87a8 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -52,6 +52,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx b/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx new file mode 100644 index 00000000..98fa9366 --- /dev/null +++ b/Oqtane.Client/Resources/Themes/Controls/CookieConsent.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + <div class="gdpr-consent-bar bg-light text-dark p-3 fixed-bottom"> + <div class="container-fluid d-flex justify-content-between align-items-center"> + <div> + By clicking "Accept", you agree us to use cookies to ensure you get the best experience on our website. + </div> + <button class="btn btn-primary" type="submit">Accept</button> + </div> + </div> + + + \ No newline at end of file diff --git a/Oqtane.Client/Services/CookieConsentService.cs b/Oqtane.Client/Services/CookieConsentService.cs new file mode 100644 index 00000000..53e7d124 --- /dev/null +++ b/Oqtane.Client/Services/CookieConsentService.cs @@ -0,0 +1,31 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System; +using Oqtane.Documentation; +using Oqtane.Shared; +using System.Globalization; + +namespace Oqtane.Services +{ + /// + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class CookieConsentService : ServiceBase, ICookieConsentService + { + public CookieConsentService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string ApiUrl => CreateApiUrl("CookieConsent"); + + /// + public async Task CanTrackAsync() + { + return await GetJsonAsync($"{ApiUrl}/CanTrack"); + } + + public async Task CreateConsentCookieAsync() + { + var cookie = await GetStringAsync($"{ApiUrl}/CreateConsentCookie"); + return cookie ?? string.Empty; + } + } +} diff --git a/Oqtane.Client/Services/Interfaces/ICookieConsentService.cs b/Oqtane.Client/Services/Interfaces/ICookieConsentService.cs new file mode 100644 index 00000000..833d68a3 --- /dev/null +++ b/Oqtane.Client/Services/Interfaces/ICookieConsentService.cs @@ -0,0 +1,24 @@ +using Oqtane.Models; +using System; +using System.Threading.Tasks; + +namespace Oqtane.Services +{ + /// + /// Service to retrieve cookie consent information. + /// + public interface ICookieConsentService + { + /// + /// Get cookie consent status + /// + /// + Task CanTrackAsync(); + + /// + /// Grant cookie consent + /// + /// + Task CreateConsentCookieAsync(); + } +} diff --git a/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor b/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor index 2c684bb8..0dcf974a 100644 --- a/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor +++ b/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor @@ -26,9 +26,11 @@
+
+ @code { diff --git a/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor new file mode 100644 index 00000000..a6612e33 --- /dev/null +++ b/Oqtane.Client/Themes/Controls/Theme/CookieConsent.razor @@ -0,0 +1,33 @@ +@namespace Oqtane.Themes.Controls +@inherits ThemeControlBase +@inject ICookieConsentService CookieConsentService +@inject IJSRuntime JSRuntime +@inject IStringLocalizer Localizer + +@if (showBanner) +{ +
+ + @((MarkupString)Convert.ToString(Localizer["ConsentBody"])) +
+} +@code { + private bool showBanner; + + protected override async Task OnInitializedAsync() + { + showBanner = !(await CookieConsentService.CanTrackAsync()); + } + + private async Task AcceptPolicy() + { + var cookieString = await CookieConsentService.CreateConsentCookieAsync(); + if (!string.IsNullOrEmpty(cookieString)) + { + var interop = new Interop(JSRuntime); + await interop.SetCookieString(cookieString); + + showBanner = false; + } + } +} \ No newline at end of file diff --git a/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor b/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor index 4814ad2a..4cd232ba 100644 --- a/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor +++ b/Oqtane.Client/Themes/OqtaneTheme/Themes/Default.razor @@ -107,6 +107,7 @@ { } + diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index 575889b3..8d547da7 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -37,6 +37,19 @@ namespace Oqtane.UI } } + public Task SetCookieString(string cookieString) + { + try + { + _jsRuntime.InvokeVoidAsync("Oqtane.Interop.setCookieString", cookieString); + return Task.CompletedTask; + } + catch + { + return Task.CompletedTask; + } + } + public ValueTask GetCookie(string name) { try diff --git a/Oqtane.Server/Controllers/CookieConsentController.cs b/Oqtane.Server/Controllers/CookieConsentController.cs new file mode 100644 index 00000000..7483c0b3 --- /dev/null +++ b/Oqtane.Server/Controllers/CookieConsentController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Oqtane.Models; +using Oqtane.Shared; +using System; +using System.Globalization; +using Oqtane.Infrastructure; +using Oqtane.Services; +using System.Threading.Tasks; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class CookieConsentController : Controller + { + private readonly ICookieConsentService _cookieConsentService; + + public CookieConsentController(ICookieConsentService cookieConsentService) + { + _cookieConsentService = cookieConsentService; + } + + [HttpGet("CanTrack")] + public async Task CanTrack() + { + return await _cookieConsentService.CanTrackAsync(); + } + + [HttpGet("CreateConsentCookie")] + public async Task CreateConsentCookie() + { + return await _cookieConsentService.CreateConsentCookieAsync(); + } + } +} diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index b04f2061..52bb3d3e 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -103,6 +103,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Server/Services/CookieConsentService.cs b/Oqtane.Server/Services/CookieConsentService.cs new file mode 100644 index 00000000..afc1dbe1 --- /dev/null +++ b/Oqtane.Server/Services/CookieConsentService.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics.Contracts; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Localization; +using Oqtane.Documentation; + +namespace Oqtane.Services +{ + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class ServerCookieConsentService : ICookieConsentService + { + private readonly IHttpContextAccessor _accessor; + + public ServerCookieConsentService(IHttpContextAccessor accessor) + { + _accessor = accessor; + } + + public Task CanTrackAsync() + { + var consentFeature = _accessor.HttpContext?.Features.Get(); + var canTrack = consentFeature?.CanTrack ?? true; + + return Task.FromResult(canTrack); + } + + public Task CreateConsentCookieAsync() + { + var consentFeature = _accessor.HttpContext?.Features.Get(); + consentFeature?.GrantConsent(); + var cookie = consentFeature?.CreateConsentCookie() ?? string.Empty; + + return Task.FromResult(cookie); + } + } +} diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 8ab42dd8..b2ad5487 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -169,6 +169,13 @@ namespace Oqtane options.CustomSchemaIds(type => type.ToString()); // Handle SchemaId already used for different type }); services.TryAddSwagger(_useSwagger); + + //add cookie consent policy + services.Configure(options => + { + options.CheckConsentNeeded = context => true; + options.ConsentCookieValue = Constants.CookieConsentCookieValue; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -225,6 +232,7 @@ namespace Oqtane app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); + app.UseCookiePolicy(); if (_useSwagger) { diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 719eb63e..191d9823 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -14,6 +14,9 @@ Oqtane.Interop = { } document.cookie = cookieString; }, + setCookieString: function (cookieString) { + document.cookie = cookieString; + }, getCookie: function (name) { name = name + "="; var decodedCookie = decodeURIComponent(document.cookie); diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 024fc1a7..b4ef8379 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -91,6 +91,7 @@ namespace Oqtane.Shared public const string BootstrapStylesheetUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"; public const string BootstrapStylesheetIntegrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg=="; + public const string CookieConsentCookieValue = "true"; // Obsolete constants const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames";