Cookie-basierte automatische Autorisierung wurde konfiguriert. Middlevare für Benutzerberechtigung hinzugefügt

This commit is contained in:
Developer 02 2024-04-15 17:24:27 +02:00
parent 49cfeb28d9
commit 87c839549a
31 changed files with 1111 additions and 44 deletions

View File

@ -0,0 +1,24 @@
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
namespace EnvelopeGenerator.Application.Contracts
{
public interface IJWTService<TClaimValue>
{
public static SymmetricSecurityKey GenerateSecurityKey(int byteSize = 32)
{
using var rng = RandomNumberGenerator.Create();
var randomBytes = new byte[byteSize];
rng.GetBytes(randomBytes);
var securityKey = new SymmetricSecurityKey(randomBytes);
return securityKey;
}
string GenerateToken(TClaimValue claimValue);
JwtSecurityToken? ReadSecurityToken(string token);
}
}

View File

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace EnvelopeGenerator.Web.Controllers
{
public static class ControllerBaseExtensions
{
public static (string EnvelopeUuid, string ReceiverSignature)? GetAuthenticatedEnvelopeDetails(this ControllerBase controller)
{
if(controller?.User?.Identity?.IsAuthenticated ?? false)
{
var envelopeUuid = controller.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var receiverSignature = controller.User.FindFirst(ClaimTypes.Hash)?.Value;
if (!string.IsNullOrEmpty(envelopeUuid) && !string.IsNullOrEmpty(receiverSignature))
return (EnvelopeUuid: envelopeUuid, ReceiverSignature: receiverSignature);
}
return null;
}
}
}

View File

@ -1,12 +1,14 @@
using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Application.Services;
using EnvelopeGenerator.Common;
using EnvelopeGenerator.Web.Models;
using EnvelopeGenerator.Web.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using System.Diagnostics;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
namespace EnvelopeGenerator.Web.Controllers
{
@ -62,7 +64,7 @@ namespace EnvelopeGenerator.Web.Controllers
}
[HttpGet("/EnvelopeKey/{envelopeReceiverId}")]
public async Task<IActionResult> ShowEnvelope([FromRoute] string envelopeReceiverId)
public async Task<IActionResult> SendAccessCode([FromRoute] string envelopeReceiverId)
{
EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId);
@ -76,18 +78,20 @@ namespace EnvelopeGenerator.Web.Controllers
bool actionResult = database.Services.actionService.RequestAccessCode(response.Envelope, response.Receiver);
bool result = database.Services.emailService.SendDocumentAccessCodeReceivedEmail(response.Envelope, response.Receiver);
}
}
return Redirect($"/EnvelopeKey/{envelopeReceiverId}/Locked");
}
else
{
ViewData["EnvelopeKey"] = envelopeReceiverId;
return View();
}
return Redirect($"/EnvelopeKey/{envelopeReceiverId}/Locked");
}
[HttpGet("/EnvelopeKey/{envelopeReceiverId}/Locked")]
public IActionResult EnvelopeLocked([FromRoute] string envelopeReceiverId)
{
ViewData["EnvelopeKey"] = envelopeReceiverId;
return View();
}
[HttpPost("/EnvelopeKey/{envelopeReceiverId}/Locked")]
public async Task<IActionResult> ShowEnvelope([FromRoute] string envelopeReceiverId, [FromForm] string access_code)
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] string access_code)
{
var decodedId = envelopeReceiverId.DecodeEnvelopeReceiverId();
@ -99,16 +103,15 @@ namespace EnvelopeGenerator.Web.Controllers
if (verification.IsSuccess)
{
if (envelopeOldService.ReceiverAlreadySigned(response.Envelope, response.Receiver.Id) == true)
if (envelopeOldService.ReceiverAlreadySigned(response.Envelope, response.Receiver.Id))
{
return Problem(statusCode: 403);
return Redirect("/EnvelopeKey/{envelopeReceiverId}/Success");
}
var envelope = await _envelopeService.ReadByUuidAsync(uuid: decodedId.EnvelopeUuid, signature: decodedId.ReceiverSignature, withAll: true);
database.Services.actionService.EnterCorrectAccessCode(response.Envelope, response.Receiver); //for history
ViewData["EnvelopeKey"] = envelopeReceiverId;
ViewData["EnvelopeResponse"] = response;
ViewData["EnvelopeResponse"] = response;
if (response.Envelope.Documents.Count() > 0)
{
@ -119,6 +122,22 @@ namespace EnvelopeGenerator.Web.Controllers
else
ViewData["DocumentBytes"] = null;
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, decodedId.EnvelopeUuid),
new Claim(ClaimTypes.Hash, decodedId.ReceiverSignature),
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
return View("ShowEnvelope", envelope);
}
else
@ -129,19 +148,21 @@ namespace EnvelopeGenerator.Web.Controllers
}
}
[HttpGet("/EnvelopeKey/{envelopeReceiverId}/Locked")]
public async Task<IActionResult> EnvelopeLocked([FromRoute] string envelopeReceiverId)
[HttpGet("/EnvelopeKey/{envelopeReceiverId}/Success")]
public async Task<IActionResult> EnvelopeSigned(string envelopeReceiverId)
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
ViewData["EnvelopeKey"] = envelopeReceiverId;
return View();
}
[HttpGet("/EnvelopeKey/{EnvelopeReceiverId}/Success")]
public IActionResult EnvelopeSigned()
[Authorize]
[HttpGet("IsAuthenticated")]
public IActionResult IsAuthenticated()
{
ViewData["EnvelopeKey"] = HttpContext.Request.RouteValues["EnvelopeReceiverId"];
return View();
var envelopeUuid = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var receiverSignature = User.FindFirst(ClaimTypes.Hash)?.Value;
return Ok(new { EnvelopeUuid = envelopeUuid, ReceiverSignature = receiverSignature });
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
@ -149,5 +170,9 @@ namespace EnvelopeGenerator.Web.Controllers
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
[Authorize]
[HttpGet("test")]
public string Test() => "Test";
}
}

View File

@ -0,0 +1,42 @@
using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace EnvelopeGenerator.Web.Controllers.Test
{
[ApiController]
[Route("api/test/[controller]")]
public class TestAuthController : ControllerBase
{
private readonly IJWTService<string> _authService;
public TestAuthController(IJWTService<string> authService)
{
_authService = authService;
}
[HttpPost]
public IActionResult ProvideToken([FromQuery] string value)
{
var token = _authService.GenerateToken(value);
return Ok(token);
}
[HttpGet]
public IActionResult GetSecurityToken([FromQuery] string token)
{
var sToken = _authService.ReadSecurityToken(token);
return Ok(sToken);
}
[HttpGet("Username")]
[Authorize]
public IActionResult Getname()
{
var username = User.Claims?.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
return Ok(username);
}
}
}

View File

@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Web.Controllers.Test
{
//[NonController]
[ApiController]
[Route("api/test/[controller]")]
public class TestControllerBase<TOriginalController, TCRUDService, TCRUDRepository, TDto, TEntity, TId> : BasicCRUDControllerBase<TOriginalController, TCRUDService, TCRUDRepository, TDto, TEntity, TId> where TOriginalController : CRUDControllerBase<TOriginalController, TCRUDService, TCRUDRepository, TDto, TDto, TDto, TEntity, TId> where TCRUDService : ICRUDService<TCRUDRepository, TDto, TDto, TDto, TEntity, TId> where TCRUDRepository : ICRUDRepository<TEntity, TId> where TDto : class where TEntity : class

View File

@ -23,8 +23,10 @@
<PackageReference Include="Quartz.Plugins" Version="3.8.0" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.8.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="7.0.0" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.5" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -11,6 +11,8 @@ using NLog;
using Quartz;
using NLog.Web;
using DigitalData.Core.API;
using Microsoft.AspNetCore.Authentication.Cookies;
using DigitalData.Core.Application;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!");
@ -49,7 +51,7 @@ try
builder.Services.AddDbContext<EGDbContext>(options =>
options.UseSqlServer(connStr));
//Inject CRUD Service and repositories
//Inject CRUD Service and repositoriesad
builder.Services.AddScoped<IConfigRepository, ConfigRepository>();
builder.Services.AddScoped<IDocumentReceiverElementRepository, DocumentReceiverElementRepository>();
builder.Services.AddScoped<IEnvelopeDocumentRepository, EnvelopeDocumentRepository>();
@ -82,7 +84,51 @@ try
//Auto mapping profiles
builder.Services.AddAutoMapper(typeof(BasicDtoMappingProfile).Assembly);
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context =>
{
var consentCookie = context.Request.Cookies["cookie-consent-settings"];
return consentCookie != "necessary=false";
};
options.MinimumSameSitePolicy = SameSiteMode.Strict;
options.ConsentCookie.Name = "cookie-consent-settings";
});
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
// Set up event handlers for dynamic login and logout paths
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = context =>
{
// Dynamically calculate the redirection path, for example:
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}/Locked";
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
},
OnRedirectToLogout = context =>
{
// Apply a similar redirection logic for logout
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}/Success";
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
}
};
});
builder.Services.AddCookieConsentSettings();
var app = builder.Build();
// Configure the HTTP request pipeline.
@ -98,9 +144,11 @@ try
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

View File

@ -1,21 +1,13 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:30275",
"sslPort": 44372
}
},
{
"profiles": {
"EnvelopeGenerator.Web": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7141;http://localhost:5282",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7141;http://localhost:5282"
},
"IIS Express": {
"commandName": "IISExpress",
@ -26,13 +18,21 @@
},
"swagger": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7202;http://localhost:5009",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7202;http://localhost:5009"
}
},
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:30275",
"sslPort": 44372
}
}
}
}

View File

@ -4,6 +4,7 @@
@{
ViewData["Title"] = "Dokument unterschreiben";
}
<partial name="_CookieConsentPartial" />
@if (Model.IsSuccess && Model.Data is not null)
{
var envelope = Model.Data;

View File

@ -0,0 +1,24 @@
@using DigitalData.Core.Application.DTO;
@using Microsoft.AspNetCore.Http.Features
@using Newtonsoft.Json.Serialization;
@using Newtonsoft.Json;
@inject CookieConsentSettings _cookieSettings
@{
var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
var showBanner = !consentFeature?.CanTrack ?? false;
var cookieString = consentFeature?.CreateConsentCookie();
}
@if (showBanner)
{
<script>
@{
var serializerSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
string serializedProps = JsonConvert.SerializeObject(_cookieSettings, serializerSettings);
}
var props = @Html.Raw(serializedProps);
var cookieSettings = new BootstrapCookieConsentSettings(props)
</script>
}

View File

@ -19,13 +19,12 @@
<script src="~/js/network.js" asp-append-version="true"></script>
<script src="~/js/app.js" asp-append-version="true"></script>
<script src="~/lib/pspdfkit/pspdfkit.js" asp-append-version="true"></script>
<script src="~/lib/bootstrap-cookie-consent-settings-main/src/bootstrap-cookie-consent-settings.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
<main role="main">
<partial name="_CookieConsentPartial" />
@RenderBody()
</main>
@Html.AntiForgeryToken()
</body>
</html>

View File

@ -43,5 +43,30 @@
]
},
"AddTestControllers": false
"AddTestControllers": true,
"Jwt": {
"Issuer": "https://localhost:7202",
"Audience": "https://localhost:7202",
"Key": "8RGnd7x0G2TYLOIW4m_qlIls7MfbAIGNrpQJzMAUIvULHOLiG723znRa_MG-Z4yw3SErusOU4hTui2rVBMcCaQ"
},
"AuthCookieConfig": {
"HttpOnly": true,
"SecurePolicy": 1
},
"CookieConsentSettings": {
"PrivacyPolicyUrl": "./",
"LegalNoticeUrl": "./",
"ContentURL": "/lib/bootstrap-cookie-consent-settings-main/cookie-consent-content",
"ButtonAgreeClass": "btn btn-primary",
"ButtonDontAgreeClass": "btn btn-link text-decoration-none none-display",
"ButtonSaveClass": "btn btn-secondary none-display",
"Lang": "en",
"DefaultLang": "en",
"CookieName": "cookie-consent-settings",
"CookieStorageDays": 1,
"ModalId": "bootstrapCookieConsentSettingsModal",
"AlsoUseLocalStorage": false,
"Categories": [ "necessary" ]
}
}

View File

@ -135,4 +135,8 @@ footer#page-footer a:focus {
}
.envelope-message {
font-family: 'Roboto', sans-serif;
}
.none-display {
display: none
}

View File

@ -0,0 +1,4 @@
src/bootstrap-cookie-consent-settings.js linguist-vendored=false
index.html linguist-documentation
demo/legal-notice.html linguist-documentation
demo/privacy-policy.html linguist-documentation

View File

@ -0,0 +1,2 @@
/.idea
/node_modules

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Stefan Haack
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,121 @@
# bootstrap-cookie-consent-settings
A modal dialog (cookie banner) and framework to handle the EU law (as written by EuGH, 1.10.2019 C-673/17)
about cookies in a website. Needs Bootstrap 5.
- [Demo page](https://shaack.com/projekte/bootstrap-cookie-consent-settings)
- [GitHub Repository](https://github.com/shaack/bootstrap-cookie-consent-settings)
- [npm package](https://www.npmjs.com/package/bootstrap-cookie-consent-settings)
## Usage
### Construct
Initialize the cookie consent framework with the constructor
```js
var cookieSettings = new BootstrapCookieConsentSettings(props)
```
You should configure the framework with the `props` object, at least the properties `privacyPolicyUrl`, `legalNoticeUrl`
and `contentURL`. The default configuration is
```js
this.props = {
privacyPolicyUrl: undefined, // the URL of your privacy policy page
legalNoticeUrl: undefined, // the URL of you legal notice page (Impressum)
contentURL: "/cookie-consent-content", // this folder must contain the language-files in the needed languages (`[lang].js`)
buttonAgreeClass: "btn btn-primary", // the "Agree to all" buttons class
buttonDontAgreeClass: "btn btn-link text-decoration-none", // the "I do not agree" buttons class
buttonSaveClass: "btn btn-secondary", // the "Save selection" buttons class
autoShowModal: true, // disable autoShowModal on the privacy policy and legal notice pages, to make these pages readable
alsoUseLocalStorage: true, // if true, the settings are stored in localStorage, too
postSelectionCallback: undefined, // callback function, called after the user has saved the settings
lang: navigator.language, // the language, in which the modal is shown
defaultLang: "en", // default language, if `lang` is not available as translation in `cookie-consent-content`
categories: ["necessary", "statistics", "marketing", "personalization"], // the categories for selection, must be contained in the language files
cookieName: "cookie-consent-settings", // the name of the cookie in which the configuration is stored as JSON
cookieStorageDays: 365, // the duration the cookie configuration is stored on the client
modalId: "bootstrapCookieConsentSettingsModal" // the id of the modal dialog element
}
```
### Show dialog
On a new visit the dialog is shown automatically.
To allow the user a reconfiguration you can show the Dialog again with
```js
cookieSettings.showDialog()
```
### Read the settings in JavaScript
Read all cookie settings with
```js
cookieSettings.getSettings()
```
It should return some JSON like
```json
{
"necessary": true,
"statistics": true,
"marketing": true,
"personalization": true
}
```
or `undefined`, before the user has chosen his cookie options.
Read a specific cookie setting with
```js
cookieSettings.getSettings('statistics')
```
for the `statistics` cookie settings. Also returns `undefined`, before the user has chosen his cookie options.
### Read the settings from the backend
You can read the settings with all server languages, you just have to read the cookie `cookie-consent-settings`.
The content of the cookie is encoded like a http query string.
```
necessary=true&statistics=false&marketing=true&personalization=true
```
#### PHP helper class
I provide a PHP helper class that you can use to read and write the settings from a PHP backend.
It is located in `php/Shaack/BootstrapCookieConsentSettings.php`.
You can use it as described in the following example.
```PHP
$cookieSettings = new \Shaack\BootstrapCookieConsentSettings();
// read all settings
$allSettings = $cookieSettings->getSettings();
// read a specific setting
$statisticsAllowed = $cookieSettings->getSetting("statistics");
// write a specific setting
$cookieSettings->setSetting("statistics", false);
```
### Internationalization
You find the language files in `./cookie-consent-content`. You can add here language files or modify the existing. If
you add language files please consider a pull request to also add them in this repository. Thanks.
## Disclaimer
You can use this banner for your website free of charge under the [MIT-License](./LICENSE).
The banner and framework was designed in cooperation with data protection officers and lawyers. However, we can not
guarantee whether the banner is correct for your website and assume no liability for its use.
bootstrap-cookie-consent-settings is a project of [shaack.com](https://shaack.com).

View File

@ -0,0 +1,31 @@
{
"title": "Privatsphäre Einstellungen",
"body": "Wir nutzen Cookies und ähnliche Technologien, die zum Betrieb der Website erforderlich sind. Zusätzliche Cookies werden nur mit Ihrer Zustimmung verwendet. Es steht Ihnen frei, Ihre Zustimmung jederzeit zu geben, zu verweigern oder zurückzuziehen, indem Sie den Link \"Cookie-Einstellungen\" unten auf jeder Seite nutzen. Sie können der Verwendung von Cookies durch uns zustimmen, indem Sie auf \"Einverstanden\" klicken. Für weitere Informationen darüber, welche Daten gesammelt und wie sie an unsere Partner weitergegeben werden, lesen Sie bitte unsere --privacy-policy--.",
"privacyPolicy": "Datenschutzerklärung",
"legalNotice": "Impressum",
"mySettings": "Meine Einstellungen",
"buttonNotAgree": "Ich bin nicht einverstanden",
"buttonAgree": "Einverstanden",
"buttonSaveSelection": "Auswahl speichern",
"buttonAgreeAll": "Allen zustimmen",
"categories": {
"necessary": {
"title": "Notwendig",
"description": ["Zum Betrieb der Website erforderlich"]
},
"statistics": {
"title": "Statistik",
"description": ["Beobachtung der Website-Nutzung und Optimierung der Benutzererfahrung"]
},
"marketing": {
"title": "Marketing",
"description": ["Bewertung von Marketingaktionen"]
},
"personalization": {
"title": "Personalisierung",
"description": ["Speicherung Ihrer Präferenzen aus früheren Besuchen",
"Sammeln von Benutzer-Feedback zur Verbesserung unserer Website",
"Erfassung Ihrer Interessen, um maßgeschneiderte Inhalte und Angebote anzubieten"]
}
}
}

View File

@ -0,0 +1,31 @@
{
"title": "Privacy Settings",
"body": "We use cookies and similar technologies that are necessary to operate the website. Additional cookies are only used with your consent. You are free to give, deny, or withdraw your consent at any time by using the \"cookie settings\" link at the bottom of each page. You can consent to our use of cookies by clicking \"Agree\". For more information about what information is collected and how it is shared with our partners, please read our --privacy-policy--.",
"privacyPolicy": "Data Protection Statement",
"legalNotice": "Legal Notice",
"mySettings": "My Settings",
"buttonNotAgree": "I do not agree",
"buttonAgree": "Agree",
"buttonSaveSelection": "Save selection",
"buttonAgreeAll": "Agree to all",
"categories": {
"necessary": {
"title": "Necessary",
"description": ["Required to run the website"]
},
"statistics": {
"title": "Statistics",
"description": ["Monitoring website usage and optimizing the user experience"]
},
"marketing": {
"title": "Marketing",
"description": ["Evaluation of marketing actions"]
},
"personalization": {
"title": "Personalization",
"description": ["Storage of your preferences from previous visits",
"Collecting user feedback to improve our website",
"Recording of your interests in order to provide customised content and offers"]
}
}
}

View File

@ -0,0 +1,31 @@
{
"title": "Paramètres de confidencialitat",
"body": "Utilizam de cookies e de tecnologias similaras que fan mestièr pel foncionament del site web. De cookies addicionals son sonque utilizats amb vòstre acòrd. Sètz liure de donar, refusar o tirar vòstre acòrd a tot moment en utilizant lo ligam «Paramètres de cookies» enbàs de cada pagina. Podètz consentir a nòstra utilizacion dels cookies en clicant «Acceptar». Per mai d'informacions tocant quina informacion es collectada e partejada amb nòstre socis, vejatz nòstra --privacy-policy--.",
"privacyPolicy": "declaracion de proteccion de las donadas",
"legalNotice": "Mencions legalas",
"mySettings": "Mos paramètres",
"buttonNotAgree": "Soi pas d'acòrd",
"buttonAgree": "Acceptar",
"buttonSaveSelection": "Enregistrar la seleccion",
"buttonAgreeAll": "Tot acceptar",
"categories": {
"necessary": {
"title": "Necessaris",
"description": ["Requerits pel foncionament del site"]
},
"statistics": {
"title": "Estatisticas",
"description": ["Per susvelhar l'utilizacion del site e melhorar l'experiéncia dels utilizaires"]
},
"marketing": {
"title": "Marketing",
"description": ["Estudi de las accions de marketing"]
},
"personalization": {
"title": "Personalizacion",
"description": ["Gardar las preferéncias de visitas precedentas",
"Reculhir los comentaris dels utilizaire per melhorar nòstre site web",
"Enregistrar vòstres interesses per vos fornir de contenguts e publicitats personalizats<"]
}
}
}

View File

@ -0,0 +1,79 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
<title>bootstrap-cookie-banner demo page</title>
</head>
<body>
<div class="container">
<h1 class="mt-5 mb-4"><a href="../">bootstrap-cookie-consent-settings</a> demo page</h1>
<p>This is a modal dialog (cookie banner) and framework to handle the German and EU law (as written by EuGH,
1.10.2019 C-673/17) about cookies in a website. This banner requires Bootstrap.</p>
<h2>Usage</h2>
<h3>Construct</h3>
<p>Initialize the cookie consent framework with the constructor</p>
<p><code>var cookieSettings = new BootstrapCookieConsent()</code></p>
<h3>Show the Dialog</h3>
<p>On a new visit the dialog is shown automatically. For reconfiguration
show the Dialog again with <code>cookieSettings.showDialog()</code>.
</p>
<p>
<a href="javascript:showSettingsDialog()" class="btn btn-primary">Cookie Settings</a>
</p>
<h3>Read the settings</h3>
<p>Read all cookie settings with <code>cookieSettings.getSettings()</code></p>
<div class="card mb-4">
<div class="card-body">
<pre class="mb-0" id="settingsOutput"></pre>
</div>
</div>
<p>Read a specific cookie setting with <code>cookieSettings.getSettings('statistics')</code></p>
<div class="card">
<div class="card-body">
<pre class="mb-0" id="settingsAnalysisOutput"></pre>
</div>
</div>
<h2 class="mt-4">The code of this banner</h2>
<div class="card">
<div class="card-body">
<pre><code>var cookieSettings = new BootstrapCookieConsentSettings({
contentURL: "../cookie-consent-content",
privacyPolicyUrl: "privacy-policy.html",
legalNoticeUrl: "legal-notice.html",
postSelectionCallback: function () {
location.reload() // reload after selection
}
})</code></pre>
</div>
</div>
<h2 class="mt-4">More documentation</h2>
<p class="mb-5"><a href="https://github.com/shaack/bootstrap-cookie-consent-settings">Find more documentation and
the sourcecode on GitHub</a></p>
</div>
<script src="../src/bootstrap-cookie-consent-settings.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj"
crossorigin="anonymous"></script>
<script>
const cookieSettings = new BootstrapCookieConsentSettings({
contentURL: "../cookie-consent-content",
privacyPolicyUrl: "privacy-policy.html",
legalNoticeUrl: "legal-notice.html",
postSelectionCallback: function () {
location.reload() // reload after selection
}
})
function showSettingsDialog() {
cookieSettings.showDialog()
}
document.getElementById("settingsOutput").innerText = JSON.stringify(cookieSettings.getSettings())
document.getElementById("settingsAnalysisOutput").innerText = cookieSettings.getSettings("statistics")
</script>
</body>
</html>

View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
<title>bootstrap-cookie-banner demo page</title>
</head>
<body>
<div class="container">
<h1><a href="../">bootstrap-cookie-consent-settings</a></h1>
<p>This is the legal notice dummy page.</p>
<p>You have to set the legal notice URL in your content files.</p>
</div>
</body>
</html>

View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
<title>bootstrap-cookie-banner demo page</title>
</head>
<body>
<div class="container">
<h1><a href="../">bootstrap-cookie-consent-settings</a></h1>
<p>This is the privacy policy dummy page.</p>
<p>You have to set the privacy policy URL in your content files.</p>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,34 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
<title>bootstrap-cookie-consent-settings demo page</title>
</head>
<body>
<div class="container mt-5">
<h1>bootstrap-cookie-consent-settings demo page</h1>
<p>This is a modal dialog (cookie banner) and framework to handle the German and EU law (as written by EuGH,
1.10.2019 C-673/17) about cookies in a website. <b>This banner requires Bootstrap.</b></p>
<p>We also have <b>another</b>, smaller cookie banner, <b>without dependencies</b>, which
does not offer the user an advanced configuration. You can find it in GitHub as
<a href="https://github.com/shaack/cookie-consent-js">cookie-consent-js</a>.</p>
<h2>Example</h2>
<ul>
<li><a href="examples/cookie-banner-example.html">Cookie consent banner with reload after selection</a></li>
</ul>
<h2>Further Information</h2>
<ul>
<li>
<a href="https://github.com/shaack/bootstrap-cookie-consent-settings">GitHub Repository and
Documentation</a>
</li>
<li>
<a href="https://www.npmjs.com/package/bootstrap-cookie-consent-settings">npm package</a>
</li>
</ul>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
{
"name": "bootstrap-cookie-consent-settings",
"version": "4.1.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "bootstrap-cookie-consent-settings",
"version": "4.1.4",
"license": "MIT"
}
}
}

View File

@ -0,0 +1,28 @@
{
"name": "bootstrap-cookie-consent-settings",
"version": "4.1.4",
"description": "Settings dialog in Bootstrap 5 and framework to handle the EU law (may 2020) about cookies in a website",
"browser": "./src/bootstrap-cookie-banner.js",
"scripts": {
"test": "echo \"No test specified\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/shaack/bootstrap-cookie-banner.git"
},
"keywords": [
"bootstrap",
"cookie",
"consent",
"eu",
"gdpr",
"dsgvo"
],
"author": "Stefan Haack (shaack.com)",
"license": "MIT",
"bugs": {
"url": "https://github.com/shaack/bootstrap-cookie-banner/issues"
},
"homepage": "https://shaack.com",
"main": "src/bootstrap-cookie-consent-settings.js"
}

View File

@ -0,0 +1,64 @@
<?php
namespace Shaack;
/**
* Helper class, to handle the `bootstrap-cookie-content-settings` from a PHP backend.
* @author Stefan Haack (https://shaack.com)
*/
class BootstrapCookieConsentSettings {
private string $cookieName;
private int $cookieStorageDays;
public function __construct(string $cookieName = "cookie-consent-settings", int $cookieStorageDays = 365)
{
$this->cookieName = $cookieName;
$this->cookieStorageDays = $cookieStorageDays;
}
/**
* Read the whole consent cookie into an array.
* @return array
*/
public function getSettings() : array {
parse_str(@$_COOKIE[$this->cookieName], $array);
return array_map(function($value) {
return $value === "true";
}, $array);
}
/**
* Write a value to the consent cookie.
* @param string $optionName
* @return bool
*/
public function getSetting(string $optionName) : bool {
return !!$this->getSettings()[$optionName];
}
/**
* Write an array of values to the consent cookie.
* @param array $settings
* @return void
*/
public function setSettings(array $settings) : void {
$settings["necessary"] = true;
$encoded = http_build_query(array_map(function($value) {
return $value ? "true" : "false";
},$settings), "", "&");
setrawcookie($this->cookieName, $encoded, time() + (86400 * $this->cookieStorageDays), "/");
}
/**
* Read a value from the consent cookie.
* @param string $optionName
* @param bool $value
* @return void
*/
public function setSetting(string $optionName, bool $value) : void {
$settings = $this->getSettings();
$settings[$optionName] = $value;
$this->setSettings($settings);
}
}

View File

@ -0,0 +1,359 @@
/**
* Author and copyright: Stefan Haack (https://shaack.com)
* Repository: https://github.com/shaack/bootstrap-cookie-consent-settings
* License: MIT, see file 'LICENSE'
*/
"use strict"
function BootstrapCookieConsentSettings(props) {
const self = this
let detailedSettingsShown = false
this.props = {
privacyPolicyUrl: undefined, // the URL of your privacy policy page
legalNoticeUrl: undefined, // the URL of you legal notice page (Impressum)
contentURL: "/cookie-consent-content", // this folder must contain the language-files in the needed languages (`[lang].js`)
buttonAgreeClass: "btn btn-primary", // the "Agree to all" buttons class
buttonDontAgreeClass: "btn btn-link text-decoration-none", // the "I do not agree" buttons class
buttonSaveClass: "btn btn-secondary", // the "Save selection" buttons class
autoShowModal: true, // disable autoShowModal on the privacy policy and legal notice pages, to make these pages readable
alsoUseLocalStorage: true, // if true, the settings are stored in localStorage, too
postSelectionCallback: undefined, // callback function, called after the user has saved the settings
lang: navigator.language, // the language, in which the modal is shown
defaultLang: "en", // default language, if `lang` is not available as translation in `cookie-consent-content`
categories: ["necessary", "statistics", "marketing", "personalization"], // the categories for selection, must be contained in the language files
cookieName: "cookie-consent-settings", // the name of the cookie in which the configuration is stored as JSON
cookieStorageDays: 365, // the duration the cookie configuration is stored on the client
modalId: "bootstrapCookieConsentSettingsModal" // the id of the modal dialog element
}
if (!props.privacyPolicyUrl) {
console.error("please set `privacyPolicyUrl` in the props of BootstrapCookieConsentSettings")
}
if (!props.legalNoticeUrl) {
console.error("please set `legalNoticeUrl` in the props of BootstrapCookieConsentSettings")
}
for (const property in props) {
// noinspection JSUnfilteredForInLoop
this.props[property] = props[property]
}
this.lang = this.props.lang
if (this.lang.indexOf("-") !== -1) {
this.lang = this.lang.split("-")[0]
}
// read the cookie, and if its content does not fit the categories, remove it
const cookieContent = getCookie(this.props.cookieName)
if (cookieContent) {
try {
for (const category of this.props.categories) {
if (cookieContent[category] === undefined) {
console.log("cookie settings changed, removing settings cookie")
removeCookie(this.props.cookieName)
break
}
}
} catch (e) {
// problems with the cookie, remove it
console.warn("cookie settings changed, removing settings cookie", e)
removeCookie(this.props.cookieName)
}
}
/**
* Read the language file and render the modal
*/
fetchContent(self.lang, (result) => {
self.content = JSON.parse(result)
renderModal()
})
function renderModal() {
const _t = self.content
const linkPrivacyPolicy = '<a href="' + self.props.privacyPolicyUrl + '">' + _t.privacyPolicy + '</a>'
const linkLegalNotice = '<a href="' + self.props.legalNoticeUrl + '">' + _t.legalNotice + '</a>'
if (self.content[self.lang] === undefined) {
self.lang = self.props.defaultLang
}
self.content.body = self.content.body.replace(/--privacy-policy--/, linkPrivacyPolicy)
let optionsHtml = ""
for (const category of self.props.categories) {
const categoryContent = self.content.categories[category]
if (!categoryContent) {
console.error("no content for category", category, "found in language file", self.lang)
}
let descriptionList = ""
for (const descriptionElement of categoryContent.description) {
descriptionList += `<li>${descriptionElement}</li>`
}
optionsHtml += `<div class="bccs-option" data-name="${category}">
<div class="form-check mb-1">
<input type="checkbox" class="form-check-input" id="bccs-checkbox-${category}">
<label class="form-check-label" for="bccs-checkbox-${category}"><b>${categoryContent.title}</b></label>
</div>
<ul>
${descriptionList}
</ul>
</div>`
}
self.modalContent = `<!-- cookie banner => https://github.com/shaack/bootstrap-cookie-consent-settings -->
<div class="modal-dialog modal-lg shadow" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">${self.content.title}</h4>
</div>
<div class="modal-body">
<div class="bccs-body-text" style="font-size: 80%">
<p>${self.content.body}</p>
</div>
<p class="d-flex justify-content-between mb-0">
${linkLegalNotice}
<a href="#bccs-options" data-bs-toggle="collapse">${self.content.mySettings}</a>
</p>
<div id="bccs-options" class="collapse">
<div class="mt-4">
${optionsHtml}
</div>
</div>
</div>
<div class="modal-footer">
<button id="bccs-buttonDoNotAgree" type="button"
class="${self.props.buttonDontAgreeClass}">
${self.content.buttonNotAgree}
</button>
<button id="bccs-buttonAgree" type="button" class="${self.props.buttonAgreeClass}">${self.content.buttonAgree}</button>
<button id="bccs-buttonSave" type="button" class="${self.props.buttonSaveClass}">
${self.content.buttonSaveSelection}
</button>
<button id="bccs-buttonAgreeAll" type="button" class="${self.props.buttonAgreeClass}">${self.content.buttonAgreeAll}</button>
</div>
</div>
</div>`
if (!getCookie(self.props.cookieName) && self.props.autoShowModal) {
showDialog()
}
}
function showDialog() {
documentReady(function () {
self.modalElement = document.getElementById(self.props.modalId)
if (!self.modalElement) {
self.modalElement = document.createElement("div")
self.modalElement.id = self.props.modalId
self.modalElement.setAttribute("class", "modal fade")
self.modalElement.setAttribute("tabindex", "-1")
self.modalElement.setAttribute("role", "dialog")
self.modalElement.setAttribute("aria-labelledby", self.props.modalId)
self.modalElement.innerHTML = self.modalContent
document.body.append(self.modalElement)
if (self.props.postSelectionCallback) {
self.modalElement.addEventListener("hidden.bs.modal", function () {
self.props.postSelectionCallback()
})
}
self.modal = new bootstrap.Modal(self.modalElement, {
backdrop: "static",
keyboard: false
})
self.modal.show()
self.buttonDoNotAgree = self.modalElement.querySelector("#bccs-buttonDoNotAgree")
self.buttonAgree = self.modalElement.querySelector("#bccs-buttonAgree")
self.buttonSave = self.modalElement.querySelector("#bccs-buttonSave")
self.buttonAgreeAll = self.modalElement.querySelector("#bccs-buttonAgreeAll")
updateButtons()
updateOptionsFromCookie()
self.modalElement.querySelector("#bccs-options").addEventListener("hide.bs.collapse", function () {
detailedSettingsShown = false
updateButtons()
})
self.modalElement.querySelector("#bccs-options").addEventListener("show.bs.collapse", function () {
detailedSettingsShown = true
updateButtons()
})
self.buttonDoNotAgree.addEventListener("click", function () {
doNotAgree()
})
self.buttonAgree.addEventListener("click", function () {
agreeAll()
})
self.buttonSave.addEventListener("click", function () {
saveSettings()
})
self.buttonAgreeAll.addEventListener("click", function () {
agreeAll()
})
} else {
self.modal.show()
}
}.bind(this))
}
function updateOptionsFromCookie() {
const settings = self.getSettings()
if (settings) {
for (let setting in settings) {
const checkboxElement = self.modalElement.querySelector("#bccs-checkbox-" + setting)
checkboxElement.checked = settings[setting] === "true"
}
}
const checkboxNecessary = self.modalElement.querySelector("#bccs-checkbox-necessary")
checkboxNecessary.checked = true
checkboxNecessary.disabled = true
}
function updateButtons() {
if (detailedSettingsShown) {
self.buttonDoNotAgree.style.display = "none"
self.buttonAgree.style.display = "none"
self.buttonSave.style.removeProperty("display")
self.buttonAgreeAll.style.removeProperty("display")
} else {
self.buttonDoNotAgree.style.removeProperty("display")
self.buttonAgree.style.removeProperty("display")
self.buttonSave.style.display = "none"
self.buttonAgreeAll.style.display = "none"
}
}
function gatherOptions(setAllTo = undefined) {
const options = {}
for (const category of self.props.categories) {
if (setAllTo === undefined) {
const checkbox = self.modalElement.querySelector("#bccs-checkbox-" + category)
if (!checkbox) {
console.error("checkbox not found for category", category)
}
options[category] = checkbox.checked
} else {
options[category] = setAllTo
}
}
options["necessary"] = true // necessary is necessary
return options
}
function agreeAll() {
setCookie(self.props.cookieName, gatherOptions(true), self.props.cookieStorageDays)
self.modal.hide()
}
function doNotAgree() {
setCookie(self.props.cookieName, gatherOptions(false), self.props.cookieStorageDays)
self.modal.hide()
}
function saveSettings() {
setCookie(self.props.cookieName, gatherOptions(), self.props.cookieStorageDays)
self.modal.hide()
}
function fetchContent(lang, callback) {
const request = new XMLHttpRequest()
request.overrideMimeType("application/json")
const url = self.props.contentURL + '/' + lang + '.json'
request.open('GET', url, true)
request.onreadystatechange = function () {
if (request.readyState === 4 && request.status === 200) {
if (request.status === 200) {
callback(request.responseText)
} else {
console.error(url, request.status)
}
}
}
request.onloadend = function () {
if (request.status === 404 && lang !== self.props.defaultLang) {
console.warn("language " + lang + " not found trying defaultLang " + self.props.defaultLang)
fetchContent(self.props.defaultLang, callback)
}
}
request.send(null)
}
function setCookie(name, object, days) {
let expires = ""
if (days) {
const date = new Date()
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000))
expires = "; expires=" + date.toUTCString()
}
const value = new URLSearchParams(object).toString()
document.cookie = name + "=" + (value || "") + expires + "; Path=/; SameSite=Strict;"
// store value also in localStorage
localStorage.setItem(name, value)
}
function getCookie(name) {
const nameEQ = name + "="
const ca = document.cookie.split(';')
for (let i = 0; i < ca.length; i++) {
let c = ca[i]
while (c.charAt(0) === ' ') {
c = c.substring(1, c.length)
}
if (c.indexOf(nameEQ) === 0) {
const urlSearchParams = new URLSearchParams(c.substring(nameEQ.length, c.length))
const result = {}
for (const [key, value] of urlSearchParams) {
result[key] = value
}
return result
}
}
// if cookie not found, try localStorage
const value = localStorage.getItem(name)
if (value) {
const urlSearchParams = new URLSearchParams(value)
const result = {}
for (const [key, value] of urlSearchParams) {
result[key] = value
}
setCookie(name, result, self.props.cookieStorageDays)
return result
}
return null
}
function removeCookie(name) {
document.cookie = name + '=; Path=/; SameSite=Strict; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'
}
function documentReady(callback) {
if (document.readyState !== 'loading') {
callback()
} else {
document.addEventListener('DOMContentLoaded', callback)
}
}
// API
this.showDialog = function () {
showDialog()
}
this.getSettings = function (optionName) {
const cookieContent = getCookie(self.props.cookieName)
if (cookieContent) {
if (optionName === undefined) {
return cookieContent
} else {
if (cookieContent) {
return cookieContent[optionName]
} else {
return false
}
}
} else {
return undefined
}
}
this.setSetting = function (name, value) {
let settings = self.getSettings() || {}
for (const category of this.props.categories) {
if(settings[category] === undefined) {
settings[category] = true
}
}
settings[name] = value
setCookie(self.props.cookieName, settings, self.props.cookieStorageDays)
}
}