Compare commits

...

9 Commits

Author SHA1 Message Date
16bdc7820d update privacy policy (English) 2025-11-20 14:14:25 +01:00
06e32b99ea update privacy policy (German) title and last updated date 2025-11-20 14:06:51 +01:00
c7c78f96a6 refactor(PDFBurner): fix merging errors 2025-11-20 12:34:30 +01:00
5c232e61f2 merge PDFBurner changes 2025-11-20 11:57:00 +01:00
24c9321c0f bump to 3.8.0 2025-11-20 10:34:19 +01:00
c75c2b1dd5 feat(envelope-api.js): append envKey query parameter to all outgoing requests
Added automatic injection of the envKey query parameter into all request URLs within sendRequest.
Updated URL handling to use the URL API, ensuring consistent parameter merging and preventing missing envKey issues.
2025-11-20 10:33:54 +01:00
8445757f34 feat: replace default cookie events with custom EnvelopeCookieManager and introduce custom auth cookie name (env_auth) 2025-11-20 10:32:32 +01:00
b088eb089f feat(EnvelopeCookieManager): add EnvelopeCookieManager to support envelope-specific cookie names
- Introduce EnvelopeCookieManager wrapper around ChunkingCookieManager to generate dynamic cookie names based on envelopeReceiverId or envKey. Ensures request/response cookies are scoped per envelope.
2025-11-20 10:30:49 +01:00
1f745ae79c refactor(pdfburner): simplify form field handling and improve default field naming
- Replaced ImmutableDictionary-based FormFieldIndex with a simpler Dictionary
- Updated form field ordering to: NoName, signature, position, city, date
- Removed manual formFieldIndex counter, now using dictionary lookup by fieldName
- Introduced FieldNames class with NoName constant (guid-based) for unnamed fields
- Defaulted Annotation.fieldName to FieldNames.NoName instead of Nothing
2025-10-09 18:59:18 +02:00
7 changed files with 128 additions and 92 deletions

View File

@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Authentication.Cookies;
namespace EnvelopeGenerator.Web;
public class EnvelopeCookieManager : ICookieManager
{
private readonly IEnumerable<string> _envelopeKeyBasedCookieNames;
private readonly ChunkingCookieManager _inner = new();
public EnvelopeCookieManager(params string[] envelopeKeyBasedCookieNames)
{
_envelopeKeyBasedCookieNames = envelopeKeyBasedCookieNames;
}
private string GetCookieName(HttpContext context, string key)
{
if (!_envelopeKeyBasedCookieNames.Contains(key))
return key;
var envId = context.GetRouteValue("envelopeReceiverId")?.ToString();
if (string.IsNullOrEmpty(envId) && context.Request.Query.TryGetValue("envKey", out var envKeyValue))
envId = envKeyValue;
if (string.IsNullOrEmpty(envId))
return key;
return $"{key}-{envId}";
}
public string? GetRequestCookie(HttpContext context, string key)
{
var cookieName = GetCookieName(context, key);
return _inner.GetRequestCookie(context, cookieName);
}
public void AppendResponseCookie(HttpContext context, string key, string? value, CookieOptions options)
{
var cookieName = GetCookieName(context, key);
_inner.AppendResponseCookie(context, cookieName, value, options);
}
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
{
var cookieName = GetCookieName(context, key);
_inner.DeleteCookie(context, cookieName, options);
}
}

View File

@@ -12,9 +12,9 @@
<PackageTags>digital data envelope generator web</PackageTags>
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<Version>3.7.0</Version>
<AssemblyVersion>3.7.0</AssemblyVersion>
<FileVersion>3.7.0</FileVersion>
<Version>3.8.0</Version>
<AssemblyVersion>3.8.0</AssemblyVersion>
<FileVersion>3.8.0</FileVersion>
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
</PropertyGroup>

View File

@@ -17,6 +17,7 @@ using EnvelopeGenerator.Web.Models.Annotation;
using DigitalData.UserManager.DependencyInjection;
using EnvelopeGenerator.Web.Middleware;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Web;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!");
@@ -134,41 +135,22 @@ try
options.ConsentCookie.Name = "cookie-consent-settings";
});
var authCookieName = "env_auth";
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
options.Cookie.Name = authCookieName;
options.CookieManager = new EnvelopeCookieManager(authCookieName);
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.SameSite = SameSiteMode.Strict;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = context =>
{
// Dynamically calculate the redirection path, for example:
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
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}";
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
}
};
});
builder.Services.AddSingleton(config.GetSection("ContactLink").Get<ContactLink>() ?? new());
builder.Services.AddCookieBasedLocalizer();
builder.Services.AddSingleton(HtmlEncoder.Default);
builder.Services.AddSingleton(UrlEncoder.Default);
builder.Services.AddSanitizer<HtmlSanitizer>();
@@ -249,7 +231,7 @@ try
app.UseAuthorization();
var cultures = app.Services.GetRequiredService<Cultures>();
if(!cultures.Any())
if (!cultures.Any())
throw new InvalidOperationException(@"Languages section is missing in the appsettings. Please configure like following.
Language is both a name of the culture and the name of the resx file such as Resource.de-DE.resx
FIClass is the css class (in wwwroot/lib/flag-icons-main) for the flag of country.
@@ -264,7 +246,7 @@ try
}
]");
if(!config.GetValue<bool>("DisableMultiLanguage"))
if (!config.GetValue<bool>("DisableMultiLanguage"))
app.UseCookieBasedLocalizer(cultures.Languages.ToArray());
app.UseCors("SameOriginPolicy");
@@ -273,7 +255,7 @@ try
app.MapFallbackToController("Error404", "Home");
app.Run();
}
catch(Exception ex)
catch (Exception ex)
{
logger.Error(ex, "Stopped program because of exception");
throw;

View File

@@ -1,106 +1,111 @@
//#region parameters
const env = Object.freeze({
__lazyXsrfToken: new Lazy(() => document.getElementsByName('__RequestVerificationToken')[0].value),
get xsrfToken() {
return this.__lazyXsrfToken.value;
}
__lazyXsrfToken: new Lazy(() => document.getElementsByName('__RequestVerificationToken')[0].value),
get xsrfToken() {
return this.__lazyXsrfToken.value;
}
})
const url = Object.freeze({
reject: `/api/annotation/reject`,
share: `/api/readonly`
reject: `/api/annotation/reject`,
share: `/api/readonly`
});
//#endregion
//#region request helper methods
function sendRequest(method, url, body = undefined) {
const options = {
credentials: 'include',
method: method,
headers: {
'X-XSRF-TOKEN': env.xsrfToken
const urlObj = new URL(url, window.location.origin);
if (!urlObj.searchParams.has("envKey")) {
urlObj.searchParams.set("envKey", ENV_KEY);
}
}
if (body !== undefined) {
options.body = JSON.stringify(body);
options.headers['Content-Type'] = 'application/json';
}
const options = {
credentials: 'include',
method: method,
headers: {
'X-XSRF-TOKEN': env.xsrfToken
}
}
return fetch(url, options);
if (body !== undefined) {
options.body = JSON.stringify(body);
options.headers['Content-Type'] = 'application/json';
}
return fetch(urlObj, options);
}
function getRequest(url) {
return sendRequest('GET', url);
return sendRequest('GET', url);
}
function getJson(url) {
return sendRequest('GET', url).then(res => {
if (res.ok)
return res.json();
throw new Error(`Request failed with status ${res.status}`);
});
return sendRequest('GET', url).then(res => {
if (res.ok)
return res.json();
throw new Error(`Request failed with status ${res.status}`);
});
}
function postRequest(url, body = undefined) {
return sendRequest('POST', url, body);
return sendRequest('POST', url, body);
}
function reload() {
window.location.reload();
window.location.reload();
}
function redirect(url) {
window.location.href = url;
window.location.href = url;
}
//#endregion
//#region envelope
function signEnvelope(annotations) {
return postRequest(`/api/annotation`, annotations)
return postRequest(`/api/annotation`, annotations)
}
async function getAnnotationParams(leftInInch = 0, topInInch = 0, inchToPointFactor = 72) {
const annotParams = await getJson("/api/Config/Annotations");
const annotParams = await getJson("/api/Config/Annotations");
for (var key in annotParams) {
var annot = annotParams[key];
annot.width *= inchToPointFactor;
annot.height *= inchToPointFactor;
annot.left += leftInInch - 0.7;
annot.left *= inchToPointFactor;
annot.top += topInInch - 0.5;
annot.top *= inchToPointFactor;
}
for (var key in annotParams) {
var annot = annotParams[key];
annot.width *= inchToPointFactor;
annot.height *= inchToPointFactor;
annot.left += leftInInch - 0.7;
annot.left *= inchToPointFactor;
annot.top += topInInch - 0.5;
annot.top *= inchToPointFactor;
}
return annotParams;
return annotParams;
}
function rejectEnvelope(reason) {
return postRequest(url.reject, reason);
return postRequest(url.reject, reason);
}
function shareEnvelope(receiverMail, dateValid) {
return postRequest(url.share, { receiverMail: receiverMail, dateValid: dateValid });
return postRequest(url.share, { receiverMail: receiverMail, dateValid: dateValid });
}
//#endregion
async function setLanguage(language) {
const hasLang = await getJson('/api/localization/lang')
.then(langs => langs.includes(language));
const hasLang = await getJson('/api/localization/lang')
.then(langs => langs.includes(language));
if (hasLang)
postRequest(`/api/localization/lang/${language}`)
.then(response => {
if (response.redirected)
redirect(response.url);
});
if (hasLang)
postRequest(`/api/localization/lang/${language}`)
.then(response => {
if (response.redirected)
redirect(response.url);
});
}
function logout() {
return postRequest(`/auth/logout`)
.then(res => {
if (res.ok)
window.location.href = "/";
});
return postRequest(`/auth/logout`)
.then(res => {
if (res.ok)
window.location.href = "/";
});
}

View File

@@ -1 +1 @@
function sendRequest(n,t,i=undefined){const r={credentials:"include",method:n,headers:{"X-XSRF-TOKEN":env.xsrfToken}};return i!==undefined&&(r.body=JSON.stringify(i),r.headers["Content-Type"]="application/json"),fetch(t,r)}function getRequest(n){return sendRequest("GET",n)}function getJson(n){return sendRequest("GET",n).then(n=>{if(n.ok)return n.json();throw new Error(`Request failed with status ${n.status}`);})}function postRequest(n,t=undefined){return sendRequest("POST",n,t)}function reload(){window.location.reload()}function redirect(n){window.location.href=n}function signEnvelope(n){return postRequest(`/api/annotation`,n)}async function getAnnotationParams(n=0,t=0,i=72){var f,r;const u=await getJson("/api/Config/Annotations");for(f in u)r=u[f],r.width*=i,r.height*=i,r.left+=n-.7,r.left*=i,r.top+=t-.5,r.top*=i;return u}function rejectEnvelope(n){return postRequest(url.reject,n)}function shareEnvelope(n,t){return postRequest(url.share,{receiverMail:n,dateValid:t})}async function setLanguage(n){const t=await getJson("/api/localization/lang").then(t=>t.includes(n));t&&postRequest(`/api/localization/lang/${n}`).then(n=>{n.redirected&&redirect(n.url)})}function logout(){return postRequest(`/auth/logout`).then(n=>{n.ok&&(window.location.href="/")})}const env=Object.freeze({__lazyXsrfToken:new Lazy(()=>document.getElementsByName("__RequestVerificationToken")[0].value),get xsrfToken(){return this.__lazyXsrfToken.value}}),url=Object.freeze({reject:`/api/annotation/reject`,share:`/api/readonly`});
function sendRequest(n,t,i=undefined){const r=new URL(t,window.location.origin);r.searchParams.has("envKey")||r.searchParams.set("envKey",ENV_KEY);const u={credentials:"include",method:n,headers:{"X-XSRF-TOKEN":env.xsrfToken}};return i!==undefined&&(u.body=JSON.stringify(i),u.headers["Content-Type"]="application/json"),fetch(r,u)}function getRequest(n){return sendRequest("GET",n)}function getJson(n){return sendRequest("GET",n).then(n=>{if(n.ok)return n.json();throw new Error(`Request failed with status ${n.status}`);})}function postRequest(n,t=undefined){return sendRequest("POST",n,t)}function reload(){window.location.reload()}function redirect(n){window.location.href=n}function signEnvelope(n){return postRequest(`/api/annotation`,n)}async function getAnnotationParams(n=0,t=0,i=72){var f,r;const u=await getJson("/api/Config/Annotations");for(f in u)r=u[f],r.width*=i,r.height*=i,r.left+=n-.7,r.left*=i,r.top+=t-.5,r.top*=i;return u}function rejectEnvelope(n){return postRequest(url.reject,n)}function shareEnvelope(n,t){return postRequest(url.share,{receiverMail:n,dateValid:t})}async function setLanguage(n){const t=await getJson("/api/localization/lang").then(t=>t.includes(n));t&&postRequest(`/api/localization/lang/${n}`).then(n=>{n.redirected&&redirect(n.url)})}function logout(){return postRequest(`/auth/logout`).then(n=>{n.ok&&(window.location.href="/")})}const env=Object.freeze({__lazyXsrfToken:new Lazy(()=>document.getElementsByName("__RequestVerificationToken")[0].value),get xsrfToken(){return this.__lazyXsrfToken.value}}),url=Object.freeze({reject:`/api/annotation/reject`,share:`/api/readonly`});

View File

@@ -4,14 +4,14 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutzinformation für das Fernsignatursystem signFLOW</title>
<title>Datenschutzinformation für das Fernsignatursystem: signFLOW</title>
<link rel="stylesheet" href="css/privacy-policy.min.css">
</head>
<body>
<header>
<h1>Datenschutzinformation für das Fernsignatursystem signFLOW</h1>
<p><strong>Stand:</strong> 19.09.2024</p>
<p><strong>Stand:</strong> 18.11.2025</p>
</header>
<section>
@@ -55,7 +55,7 @@
<h2>3. Datenerhebung</h2>
<h3>3.1 Die folgenden Kategorien personenbezogener Daten werden verarbeitet</h3>
<ul>
<li>Namen: Vor- und Zunamen sowie Ihre digitale Unterschrift</li>
<li>Namen: Benutzername, Vor- und Zunamen sowie Ihre digitale Unterschrift</li>
<li>Kontaktdaten: Telefonnummer, Mobilfunknummer und E-Mail-Adresse</li>
<li>Technische Daten: IP-Adresse, Zeitpunkt des Zugriffs oder Zugriffsversuchs</li>
</ul>

View File

@@ -10,8 +10,8 @@
<body>
<header>
<h1>Data Protection Information for the Remote Signature System signFLOW</h1>
<p><strong>As of:</strong> 19.09.2024</p>
<h1>Data Protection Information for the Remote Signature System: signFLOW</h1>
<p><strong>As of:</strong> 18.11.2025</p>
</header>
<section>
<h2>1. General Information</h2>
@@ -53,7 +53,7 @@
<h2>3. Data Collection</h2>
<h3>3.1 The following categories of personal data are processed</h3>
<ul>
<li>Names: First and last names as well as your digital signature</li>
<li>Names: Username, first and last names as well as your digital signature</li>
<li>Contact details: Phone number, mobile phone number, and email address</li>
<li>Technical data: IP address, time of access, or access attempts</li>
</ul>