Compare commits

...

30 Commits

Author SHA1 Message Date
5707213edd Conditionally apply PDF background for read-only envelopes
When preparing the PDF for the view, only apply the Background
method if the envelope is not read-only. This ensures that
read-only envelopes are displayed without additional background
elements.
2026-01-20 10:54:31 +01:00
ad54ba9dc4 Conditionally load annotations based on READ_AND_CONFIRM
Annotations are now only loaded if READ_AND_CONFIRM is falsy.
This prevents unnecessary annotation creation in read-and-confirm
scenarios. Changes applied to both app.js and app.min.js.
2026-01-20 10:54:16 +01:00
1f233153cf Restrict page view tracking to READ_AND_CONFIRM mode
Previously, page view tracking and sessionStorage updates ran unconditionally. Now, this logic is only executed when READ_AND_CONFIRM is enabled, ensuring viewed/unviewed page state is only tracked when required. Updated both source and minified files accordingly.
2026-01-20 10:38:37 +01:00
513ec007eb Set ViewData["ReadAndConfirm"] for envelope read-only state
Added "ReadAndConfirm" to ViewData, passing the envelope's ReadOnly property to the "ShowEnvelope" view. This enables the view to adjust its behavior or UI based on whether the envelope is read-only.
2026-01-20 10:30:25 +01:00
1305714da2 Move ReadOnly property from Envelope to EnvelopeDto
The ReadOnly property logic was shifted from the Envelope entity
to the EnvelopeDto record, ensuring that read-only status is
determined at the DTO layer rather than in the data model.
2026-01-20 10:30:07 +01:00
1e90cda393 Add READ_AND_CONFIRM JS constant from ViewData flag
Introduced a READ_AND_CONFIRM JavaScript constant in _Layout.cshtml, which reflects the server-side ViewData["ReadAndConfirm"] boolean value. This enables client-side scripts to easily check if the "ReadAndConfirm" flag is set.
2026-01-20 09:54:45 +01:00
5a5cbcb14d Track viewed PDF pages and persist state in sessionStorage
Added logic to monitor which PDF pages have been viewed by the user. The list of unviewed pages and a flag for all pages viewed are stored in sessionStorage and updated as the user navigates. Changes applied to both ui.js and ui.min.js.
2026-01-19 17:06:43 +01:00
a35f06070a Remove total page count logging after PSPDFKit load
Refactored ui.js and ui.min.js to eliminate retrieval and logging of the total page count after loading the PSPDFKit instance. The code now returns the instance directly after setting up the page change event listener, reducing unnecessary logging and simplifying the load process.
2026-01-19 17:03:05 +01:00
2606066103 Add logging for page changes and total pages in PSPDFKit
Added a .then() handler to loadPSPDFKit to log the active page number on page change events and log the total number of pages after loading. This aids in debugging and tracking user navigation within the document.
2026-01-19 16:57:34 +01:00
7495e062a9 Remove EnvelopeSigningType enum and update envelope logic
Removed EnvelopeSigningType enum and related normalization logic. Added a ReadOnly property to Envelope that uses EnvelopeTypeId to determine read-only status. Envelope type handling now relies on EnvelopeTypeId (int?) instead of the enum.
2026-01-19 16:50:02 +01:00
293044bec3 Handle envelope type with framework-specific properties
Add conditional logic to map "ENVELOPE_TYPE" to either an int? EnvelopeTypeId (for .NET Framework) or a type-safe EnvelopeSigningType SigningType (for .NET), supporting both legacy and modern approaches.
2026-01-19 16:21:58 +01:00
e0ff976d21 Add unit tests for EnvelopeSigningType.Normalize method
Added ConstantsTests to verify Normalize behavior for EnvelopeSigningType, including handling of valid and out-of-range enum values using NUnit test cases. Ensures normalization logic works as expected.
2026-01-19 16:08:39 +01:00
bec45ab1f1 Refactor test namespaces to EnvelopeGenerator.Tests.Application
All test files and utilities now use the EnvelopeGenerator.Tests.Application namespace for improved organization and clarity. No functional changes were made; updates are limited to namespaces and using directives. This makes it explicit that these are application-level tests and related helpers.
2026-01-19 16:02:39 +01:00
fecd054a5c Add EnvelopeSigningType enum and Normalize extension
Introduced EnvelopeSigningType enum with WetSignature and ReadAndSign values in the EnvelopeGenerator.Domain.Constants namespace. Added EnvelopeSigningTypeExtensions with a Normalize method to standardize enum values.
2026-01-19 16:01:13 +01:00
32b488c50f Refactor test dependency resolution in DocSignedNotificationTests
Refactored DocSignedNotificationTests to use typed repository
retrieval and explicit service resolution for PsPdfKitAnnotation.
Added a Services property to TestBase for easier access to
IServiceProvider. These changes improve clarity and robustness
of test dependency management.
2026-01-19 15:56:12 +01:00
9cfdd16970 Refactor test DB config to support SQL Server and fix seeding
Refactored Fake.cs to configure both in-memory and SQL Server
database contexts for testing, using the "Default" connection
string from configuration. Added detailed EF logging and SQL
executor setup. In TestBase.cs, fixed Setup to use the correct
repository instance for seeding email templates.
2026-01-19 15:51:09 +01:00
4da5848253 Refactor test namespaces; update package version
Changed test file namespaces from EnvelopeGenerator.Tests.Application to EnvelopeGenerator.Tests for consistency. Updated DigitalData.Core.Abstraction.Application package from 1.4.0 to 1.6.0 to incorporate latest improvements.
2026-01-19 15:14:59 +01:00
88da210ba2 Update DigitalData.Core.Abstraction.Application to 1.6.0
Updated DigitalData.Core.Abstraction.Application from 1.4.0 to 1.6.0 across all relevant project files and package configs. Also updated DigitalData.Core.Infrastructure from 2.4.5 to 2.6.1 in EnvelopeGenerator.Infrastructure.csproj. No other code changes were made.
2026-01-19 15:13:53 +01:00
fc23ba840e chore(Web): bump to 3.8.2 2025-11-20 16:47:45 +01:00
140d271b28 refactor(privacy-policy): remove 6. Hinweisgebersystem 2025-11-20 16:46:31 +01:00
a3b12a6957 bump to 3.8.1 2025-11-20 14:49:10 +01:00
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
30 changed files with 264 additions and 387 deletions

View File

@@ -74,6 +74,11 @@ public record EnvelopeDto
/// </summary>
public int? EnvelopeTypeId { get; set; }
/// <summary>
///
/// </summary>
public bool ReadOnly => EnvelopeTypeId == 2;
/// <summary>
///
/// </summary>

View File

@@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />

View File

@@ -70,8 +70,8 @@
<Reference Include="DigitalData.Controls.DocumentViewer, Version=1.9.8.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Controls.DocumentViewer.1.9.8\lib\net462\DigitalData.Controls.DocumentViewer.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.6.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.6.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>

View File

@@ -3,7 +3,7 @@
<package id="AutoMapper" version="10.1.1" targetFramework="net462" />
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net462" />
<package id="DigitalData.Controls.DocumentViewer" version="1.9.8" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.4.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.6.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />

View File

@@ -72,8 +72,8 @@
<Reference Include="DevExpress.XtraEditors.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a" />
<Reference Include="DevExpress.XtraGauges.v21.2.Core, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL" />
<Reference Include="DevExpress.XtraReports.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL" />
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.6.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.6.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>

View File

@@ -2,7 +2,7 @@
<packages>
<package id="AutoMapper" version="10.1.1" targetFramework="net462" />
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.4.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.6.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />

View File

@@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using EnvelopeGenerator.Domain.Constants;
using Newtonsoft.Json;
#if NETFRAMEWORK
using System;

View File

@@ -12,7 +12,7 @@ namespace EnvelopeGenerator.Infrastructure
public EGDbContext CreateDbContext(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) // Önemli!
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.migration.json")
.Build();

View File

@@ -22,8 +22,8 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.4.5" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
<PackageReference Include="QuestPDF" Version="2025.7.1" />
</ItemGroup>

View File

@@ -5,6 +5,7 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.DependencyInjection;
namespace EnvelopeGenerator.Tests.Application;
@@ -44,9 +45,15 @@ public class DocSignedNotificationTests : TestBase
// Create envelope receiver
var envRcv = this.CreateEnvelopeReceiver(env!.Id, rcv.Id);
envRcv = await Repository.CreateAsync(envRcv, cancel);
var repo = GetRepository<EnvelopeReceiver>();
envRcv = await repo.CreateAsync(envRcv, cancel);
var envRcvDto = _mapper.Map<EnvelopeReceiverDto>(envRcv);
var docSignedNtf = envRcvDto.ToDocSignedNotification(new () { });
var annots = Services.GetRequiredService<PsPdfKitAnnotation>();
var docSignedNtf = envRcvDto.ToDocSignedNotification(annots);
var sendSignedMailHandler = Host.Services.GetRequiredService<SendSignedMailHandler>();

View File

@@ -1,8 +1,10 @@
using Bogus;
using CommandDotNet;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.UserManager.Domain.Entities;
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Histories.Commands;
@@ -11,6 +13,7 @@ using EnvelopeGenerator.Application.Users.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure;
using EnvelopeGenerator.Tests.Application;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -20,7 +23,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Tests.Application;
@@ -42,10 +44,29 @@ public class Fake
// add Application and Infrastructure services
#pragma warning disable CS0618
services.AddEnvelopeGeneratorServices(configuration);
services.AddEnvelopeGeneratorInfrastructureServices(
(sp, options) => options.UseInMemoryDatabase("EnvelopeGeneratorTestDb"),
context.Configuration
);
var cnnStrName = "Default";
var connStr = context.Configuration.GetConnectionString(cnnStrName)
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
services.AddEnvelopeGeneratorInfrastructureServices(opt =>
{
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase("EnvelopeGeneratorTestDb"));
opt.AddDbTriggerParams(context.Configuration);
opt.AddDbContext((provider, options) =>
{
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
options.UseSqlServer(connStr)
.LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
});
opt.AddSQLExecutor(executor => executor.ConnectionString = connStr);
});
var prodCnnStr = context.Configuration.GetConnectionString("Default");
services.AddDbContext<EGDbContext2Prod>(opt => opt.UseSqlServer(prodCnnStr));

View File

@@ -24,6 +24,8 @@ public abstract class TestBase : Faker
protected IRepository Repository => Host.Services.GetRequiredService<IRepository>();
protected IServiceProvider Services => Host.Services;
protected abstract void ConfigureServices(IServiceCollection services);
[SetUp]
@@ -32,9 +34,11 @@ public abstract class TestBase : Faker
Host = Fake.CreateHost(ConfigureServices);
await Host.AddSamples();
var repo = GetRepository<EmailTemplate>();
// Add seed email templates
foreach (var temp in SeedEmailTemplates)
await Repository.CreateAsync(temp);
await repo.CreateAsync(temp);
}
[TearDown]

View File

@@ -0,0 +1,17 @@
using EnvelopeGenerator.Domain.Constants;
using NUnit.Framework;
namespace EnvelopeGenerator.Tests.Domain;
public class ConstantsTests
{
[TestCase(EnvelopeSigningType.ReadAndSign, EnvelopeSigningType.ReadAndSign)]
[TestCase(EnvelopeSigningType.WetSignature, EnvelopeSigningType.WetSignature)]
[TestCase((EnvelopeSigningType)5, EnvelopeSigningType.WetSignature)]
public void Normalize_ReturnsExpectedValue(EnvelopeSigningType input, EnvelopeSigningType expected)
{
var normalized = input.Normalize();
Assert.That(normalized, Is.EqualTo(expected));
}
}

View File

@@ -20,7 +20,7 @@
<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.3.0" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />

View File

@@ -248,7 +248,9 @@ public class EnvelopeController : ViewControllerBase
{
if (er.Envelope!.Documents?.FirstOrDefault() is DocumentDto doc && doc.ByteData is not null)
{
using var pdf = Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
using var pdf = er.Envelope.ReadOnly
? Pdf.FromMemory(doc.ByteData)
: Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
doc.ByteData = pdf.ExportAsBytes();
@@ -262,6 +264,8 @@ public class EnvelopeController : ViewControllerBase
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
ViewData["ReadAndConfirm"] = er.Envelope.ReadOnly;
//add PSPDFKit licence key
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];

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.2</Version>
<AssemblyVersion>3.8.2</AssemblyVersion>
<FileVersion>3.8.2</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

@@ -42,6 +42,8 @@
const IS_DESKTOP_SIZE = DEVICE_SCREEN_TYPE == 'desktop'
const IS_MOBILE_DEVICE = /Mobi|Android/i.test(window.navigator.userAgent);
const READ_AND_CONFIRM = @((ViewData["ReadAndConfirm"] is bool readAndConfirm && readAndConfirm).ToString().ToLower())
</script>
<script src="~/lib/jquery/dist/jquery.min.js"></script>

View File

@@ -35,12 +35,13 @@ class App {
});
// Load annotations into PSPDFKit
try {
let signatures = await createAnnotations(this.currentDocument, this.envelopeReceiver.envelopeId, this.envelopeReceiver.receiverId);
await this.pdfKit.create(signatures);
} catch (e) {
console.error("Error loading annotations:", e);
}
if (!READ_AND_CONFIRM)
try {
let signatures = await createAnnotations(this.currentDocument, this.envelopeReceiver.envelopeId, this.envelopeReceiver.receiverId);
await this.pdfKit.create(signatures);
} catch (e) {
console.error("Error loading annotations:", e);
}
//add click events of external buttons
[...document.getElementsByClassName('btn_refresh')].forEach(btn => btn.addEventListener('click', _ => this.handleClick('RESET')));

View File

@@ -1,3 +1,3 @@
class App{constructor(n,t,i,r,u,f){this.container=f??`#${this.constructor.name.toLowerCase()}`;this.envelopeKey=n;this.pdfKit=null;this.currentDocument=t.envelope.documents[0];this.currentReceiver=t.receiver;this.signatureCount=t.envelope.documents[0].elements.length;this.envelopeReceiver=t;this.documentBytes=i;this.licenseKey=r;this.locale=u}async init(){this.pdfKit=await loadPSPDFKit(this.documentBytes,this.container,this.licenseKey,this.locale);addToolbarItems(this.pdfKit,this.handleClick.bind(this));this.pdfKit.addEventListener("annotations.load",this.handleAnnotationsLoad.bind(this));this.pdfKit.addEventListener("annotations.change",this.handleAnnotationsChange.bind(this));this.pdfKit.addEventListener("annotations.create",this.handleAnnotationsCreate.bind(this));this.pdfKit.addEventListener("annotations.willChange",()=>{Comp.ActPanel.Toggle()});try{let n=await createAnnotations(this.currentDocument,this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId);await this.pdfKit.create(n)}catch(n){console.error("Error loading annotations:",n)}[...document.getElementsByClassName("btn_refresh")].forEach(n=>n.addEventListener("click",()=>this.handleClick("RESET")));[...document.getElementsByClassName("btn_complete")].forEach(n=>n.addEventListener("click",()=>this.handleClick("FINISH")));[...document.getElementsByClassName("btn_reject")].forEach(n=>n.addEventListener("click",()=>this.handleClick("REJECT")))}handleAnnotationsLoad(n){n.toJS()}handleAnnotationsChange(){}async handleAnnotationsCreate(n){const t=n.toJS()[0],i=!!t.formFieldName,r=!!t.isSignature;if(i===!1&&r===!0){const r=t.boundingBox.left-20,u=t.boundingBox.top-20,n=150,i=75,f=new Date,e=await createAnnotationFrameBlob(this.envelopeReceiver.name,this.currentReceiver.signature,f,n,i),o=await fetch(e),s=await o.blob(),h=await this.pdfKit.createAttachment(s),c=createImageAnnotation(new PSPDFKit.Geometry.Rect({left:r,top:u,width:n,height:i}),t.pageIndex,h,generateId(this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId,this.fakeElementId--,"signed"));this.pdfKit.create(c)}}async handleClick(n){let t=!1;switch(n){case"RESET":t=await this.handleReset(null);Comp.SignatureProgress.SignedCount=0;t.isConfirmed&&Swal.fire({title:"Erfolg",text:"Dokument wurde zurückgesetzt",icon:"info"});break;case"FINISH":t=await this.handleFinish(null);t==!0&&(window.location.href=`/Envelope/${this.envelopeKey}`);break;case"REJECT":Swal.fire({title:localized.rejection,html:`<div class="text-start fs-6 p-0 m-0">${localized.rejectionReasonQ}</div>`,icon:"question",input:"text",inputAttributes:{autocapitalize:"off"},showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.complete,cancelButtonText:localized.back,showLoaderOnConfirm:!0,preConfirm:async n=>{try{return await rejectEnvelope(n)}catch(t){Swal.showValidationMessage(`
class App{constructor(n,t,i,r,u,f){this.container=f??`#${this.constructor.name.toLowerCase()}`;this.envelopeKey=n;this.pdfKit=null;this.currentDocument=t.envelope.documents[0];this.currentReceiver=t.receiver;this.signatureCount=t.envelope.documents[0].elements.length;this.envelopeReceiver=t;this.documentBytes=i;this.licenseKey=r;this.locale=u}async init(){if(this.pdfKit=await loadPSPDFKit(this.documentBytes,this.container,this.licenseKey,this.locale),addToolbarItems(this.pdfKit,this.handleClick.bind(this)),this.pdfKit.addEventListener("annotations.load",this.handleAnnotationsLoad.bind(this)),this.pdfKit.addEventListener("annotations.change",this.handleAnnotationsChange.bind(this)),this.pdfKit.addEventListener("annotations.create",this.handleAnnotationsCreate.bind(this)),this.pdfKit.addEventListener("annotations.willChange",()=>{Comp.ActPanel.Toggle()}),!READ_AND_CONFIRM)try{let n=await createAnnotations(this.currentDocument,this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId);await this.pdfKit.create(n)}catch(n){console.error("Error loading annotations:",n)}[...document.getElementsByClassName("btn_refresh")].forEach(n=>n.addEventListener("click",()=>this.handleClick("RESET")));[...document.getElementsByClassName("btn_complete")].forEach(n=>n.addEventListener("click",()=>this.handleClick("FINISH")));[...document.getElementsByClassName("btn_reject")].forEach(n=>n.addEventListener("click",()=>this.handleClick("REJECT")))}handleAnnotationsLoad(n){n.toJS()}handleAnnotationsChange(){}async handleAnnotationsCreate(n){const t=n.toJS()[0],i=!!t.formFieldName,r=!!t.isSignature;if(i===!1&&r===!0){const r=t.boundingBox.left-20,u=t.boundingBox.top-20,n=150,i=75,f=new Date,e=await createAnnotationFrameBlob(this.envelopeReceiver.name,this.currentReceiver.signature,f,n,i),o=await fetch(e),s=await o.blob(),h=await this.pdfKit.createAttachment(s),c=createImageAnnotation(new PSPDFKit.Geometry.Rect({left:r,top:u,width:n,height:i}),t.pageIndex,h,generateId(this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId,this.fakeElementId--,"signed"));this.pdfKit.create(c)}}async handleClick(n){let t=!1;switch(n){case"RESET":t=await this.handleReset(null);Comp.SignatureProgress.SignedCount=0;t.isConfirmed&&Swal.fire({title:"Erfolg",text:"Dokument wurde zurückgesetzt",icon:"info"});break;case"FINISH":t=await this.handleFinish(null);t==!0&&(window.location.href=`/Envelope/${this.envelopeKey}`);break;case"REJECT":Swal.fire({title:localized.rejection,html:`<div class="text-start fs-6 p-0 m-0">${localized.rejectionReasonQ}</div>`,icon:"question",input:"text",inputAttributes:{autocapitalize:"off"},showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.complete,cancelButtonText:localized.back,showLoaderOnConfirm:!0,preConfirm:async n=>{try{return await rejectEnvelope(n)}catch(t){Swal.showValidationMessage(`
Request failed: ${t}
`)}},allowOutsideClick:()=>!Swal.isLoading()}).then(n=>{if(n.isConfirmed){const t=n.value;t.ok?reload():Swal.showValidationMessage(`Request failed: ${t.message}`)}});break;case"COPY_URL":const n=window.location.href.replace(/\/readonly/gi,"");navigator.clipboard.writeText(n).then(function(){bsNotify("Kopiert",{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify("Unerwarteter Fehler",{alert_type:"danger",delay:4,icon_name:"error"})});break;case"SHARE":Comp.ShareBackdrop.show();break;case"LOGOUT":await logout()}}async handleFinish(){const n=await this.pdfKit.exportInstantJSON(),t=n.formFieldValues,r=t.filter(n=>isFieldRequired(n)),u=r.some(n=>n.value===undefined||n.value===null||n.value==="");if(u)return Swal.fire({title:"Warnung",text:"Bitte füllen Sie alle Standortinformationen vollständig aus!",icon:"warning"}),!1;const f=new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$"),e=t.filter(n=>isCityField(n));for(var i of e)if(!IS_MOBILE_DEVICE&&!f.test(i.value))return Swal.fire({title:"Warnung",text:`Bitte überprüfen Sie die eingegebene Ortsangabe "${i.value}" auf korrekte Formatierung. Beispiele für richtige Formate sind: München, Île-de-France, Sauðárkrókur, San Francisco, St. Catharines usw.`,icon:"warning"}),!1;const o=await this.validateAnnotations(this.signatureCount);return o===!1?(Swal.fire({title:"Warnung",text:"Es wurden nicht alle Signaturfelder ausgefüllt!",icon:"warning"}),!1):Swal.fire({title:localized.confirmation,html:`<div class="text-start fs-6 p-0 m-0">${localized.sigAgree}</div>`,icon:"question",showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.finalize,cancelButtonText:localized.back}).then(async t=>{if(t.isConfirmed){try{await this.pdfKit.save()}catch(i){return Swal.fire({title:"Fehler",text:"Umschlag konnte nicht signiert werden!",icon:"error"}),!1}try{const t=await signEnvelope({instant:n,structured:mapSignature(n)});if(t.ok)return!0;if(t.status===409)return Swal.fire({title:"Warnung",text:"Umschlag ist nicht mehr verfügbar.",icon:"warning"}),!1;if(t.status===423)Swal.fire({title:"Info",text:"Dokument wurde von einem Empfänger abgelehnt. Sie werden weitergeleitet...",icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:"Fehler",text:"Umschlag konnte nicht signiert werden!",icon:"error"}),!1}}else return!1})}async validateAnnotations(n){const t=await getAnnotations(this.pdfKit),i=t.map(n=>n.toJS()).filter(n=>n.isSignature);return n<=i.length}async handleReset(){const n=Swal.fire({title:"Sind sie sicher?",text:"Wollen Sie das Dokument und alle erstellten Signaturen zurücksetzen?",icon:"question",showCancelButton:!0});if(n.isConfirmed){const n=await deleteAnnotations(this.pdfKit)}return n}fakeElementId=0;}

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

@@ -16,7 +16,45 @@
isEditableAnnotation: function (annotation) {
return !(annotation.isSignature || annotation.description === 'FRAME')
},
});
}).then((instance) => {
if (READ_AND_CONFIRM) {
const totalPages = instance.totalPageCount || 0
const storageKeyAll = 'pspdf_all_pages_rendered'
const storageKeyUnviewed = 'pspdf_unviewed_pages'
let unviewed = totalPages > 0 ? Array.from({ length: totalPages }, (_, i) => i + 1) : []
const saveState = () => {
sessionStorage.setItem(storageKeyUnviewed, JSON.stringify(unviewed))
sessionStorage.setItem(storageKeyAll, JSON.stringify(unviewed.length === 0 && totalPages > 0))
}
const markPageViewed = (pageIndex) => {
const pageNumber = pageIndex + 1
if (pageNumber < 1 || pageNumber > totalPages) return
const idx = unviewed.indexOf(pageNumber)
if (idx >= 0) {
unviewed.splice(idx, 1)
saveState()
}
}
// initial state in session storage
saveState()
// mark the initially visible page
const initialPage = instance.viewState?.currentPageIndex ?? 0
markPageViewed(initialPage)
instance.addEventListener('viewState.currentPageIndex.change', (pageIndex) => {
console.log('Active page:', pageIndex + 1)
markPageViewed(pageIndex)
})
}
return instance
})
}
const allowedToolbarItems = [

View File

@@ -1,4 +1,4 @@
function loadPSPDFKit(n,t,i,r){return PSPDFKit.load({inlineWorkers:!1,locale:r,licenseKey:i,styleSheets:["/css/site.css"],container:t,document:n,annotationPresets:getPresets(),electronicSignatures:{creationModes:["DRAW","TYPE","IMAGE"]},initialViewState:new PSPDFKit.ViewState({sidebarMode:PSPDFKit.SidebarMode.THUMBNAILS}),isEditableAnnotation:function(n){return!(n.isSignature||n.description==="FRAME")}})}function addToolbarItems(n,t){var i=n.toolbarItems.filter(n=>allowedToolbarItems.includes(n.type));i=IS_READONLY?i.concat(getReadOnlyItems(t)):i.concat(getWritableItems(t));IS_DESKTOP_SIZE||IS_READONLY||(i=i.concat(getMobileWritableItems(t)));n.setToolbarItems(i)}function getWritableItems(n){return[{type:"custom",id:"button-share",className:"button-share",title:"Teilen",onPress(){n("SHARE")},icon:`<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
function loadPSPDFKit(n,t,i,r){return PSPDFKit.load({inlineWorkers:!1,locale:r,licenseKey:i,styleSheets:["/css/site.css"],container:t,document:n,annotationPresets:getPresets(),electronicSignatures:{creationModes:["DRAW","TYPE","IMAGE"]},initialViewState:new PSPDFKit.ViewState({sidebarMode:PSPDFKit.SidebarMode.THUMBNAILS}),isEditableAnnotation:function(n){return!(n.isSignature||n.description==="FRAME")}}).then(n=>{if(READ_AND_CONFIRM){const t=n.totalPageCount||0,f="pspdf_all_pages_rendered",e="pspdf_unviewed_pages";let i=t>0?Array.from({length:t},(n,t)=>t+1):[];const r=()=>{sessionStorage.setItem(e,JSON.stringify(i)),sessionStorage.setItem(f,JSON.stringify(i.length===0&&t>0))},u=n=>{const u=n+1;if(!(u<1)&&!(u>t)){const f=i.indexOf(u);f>=0&&(i.splice(f,1),r())}};r();const o=n.viewState?.currentPageIndex??0;u(o);n.addEventListener("viewState.currentPageIndex.change",n=>{console.log("Active page:",n+1),u(n)})}return n})}function addToolbarItems(n,t){var i=n.toolbarItems.filter(n=>allowedToolbarItems.includes(n.type));i=IS_READONLY?i.concat(getReadOnlyItems(t)):i.concat(getWritableItems(t));IS_DESKTOP_SIZE||IS_READONLY||(i=i.concat(getMobileWritableItems(t)));n.setToolbarItems(i)}function getWritableItems(n){return[{type:"custom",id:"button-share",className:"button-share",title:"Teilen",onPress(){n("SHARE")},icon:`<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 13V17.5C20 20.5577 16 20.5 12 20.5C8 20.5 4 20.5577 4 17.5V13M12 3L12 15M12 3L16 7M12 3L8 7" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`},{type:"custom",id:"button-logout",className:"button-logout",title:"logout",onPress(){n("LOGOUT")},icon:`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-box-arrow-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M6 12.5a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v2a.5.5 0 0 1-1 0v-2A1.5 1.5 0 0 1 6.5 2h8A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 5 12.5v-2a.5.5 0 0 1 1 0z"/>

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>
@@ -162,138 +162,6 @@
<a href="https://www.bfdi.bund.de/DE/Service/Anschriften/Laender/Laender-node.html">Laender-node.html</a>
</p>
</section>
<section>
<h2>6. Hinweisgebersystem</h2>
<p>
Die Einhaltung gesetzlicher Vorschriften und interner Richtlinien, einschließlich unseres Verhaltenskodexes
sowie des Verhaltenskodexes für Geschäftspartner, hat für uns (die verarbeitende Stelle) oberste Priorität.
Dies gilt sowohl für unseren eigenen Geschäftsbereich als auch für unsere Lieferketten.
</p>
<p>
Es ist uns wichtig, Risiken frühzeitig zu identifizieren und Verstöße zu vermeiden. Wir möchten rechtzeitig
geeignete Maßnahmen ergreifen, um mögliche Schäden für Betroffene, Kunden, Mitarbeiter, Geschäftspartner und
unsere Unternehmensgruppe zu verhindern.
</p>
<p>
Aus diesem Grund haben wir ein unabhängiges, neutrales und vertrauliches Hinweisgebersystem eingerichtet,
das es internen und externen Hinweisgebenden ermöglicht, auch anonym Meldungen abzugeben. Durch unser
transparentes Beschwerdeverfahren bieten wir insbesondere den Betroffenen, den Hinweisgebenden und den
Mitarbeitenden, die an der Aufklärung der gemeldeten Vorfälle mitwirken, den größtmöglichen Schutz.
</p>
<p>
Im Rahmen dieses Verfahrens können alle tatsächlichen und vermeintlichen Verstöße gegen gesetzliche
Vorgaben, unseren Verhaltenskodex sowie den Verhaltenskodex für Geschäftspartner gemeldet werden. Auch
menschenrechtliche oder umweltbezogene Risiken sowie Pflichtverletzungen entlang der gesamten Lieferkette
unserer Konzernunternehmen und in unserem eigenen Geschäftsbereich können Gegenstand einer Meldung sein.
</p>
<p>
Einheitliche und zügige Prozesse sowie eine vertrauliche und professionelle Bearbeitung der Hinweise durch
interne Experten bilden die Grundlage dieses fairen Verfahrens. Benachteiligungen oder Bestrafungen von
Hinweisgebenden sowie von Personen, die mit der Bearbeitung von Beschwerden und Hinweisen betraut sind,
werden nicht toleriert.
</p>
<h3>6.1 Zweck und Rechtsgrundlage der Datenverarbeitung</h3>
<p>
Der Zweck der Verarbeitung personenbezogener Daten besteht in der Verwaltung des Hinweisgebersystems, das
auch die Aufdeckung schwerwiegender Verstöße oder potenzieller Verstöße gegen geltendes Recht sowie anderer
ernsthafter Angelegenheiten umfasst. Die Verarbeitung dieser Daten ist notwendig, um rechtlichen
Verpflichtungen nachzukommen, die uns auferlegt sind, gemäß Art. 6 Abs. 1 S. 1 lit. c&#41; DSGVO. Dies
bezieht
sich auf das Gesetz, das den Schutz von Hinweisgebern verbessert (Hinweisgeberschutzgesetz - HinSchG).
</p>
<p>
Zudem dient die Verarbeitung dem berechtigten Interesse, schwerwiegende Verstöße oder mögliche Verstöße
gegen geltendes Recht sowie andere ernsthafte Angelegenheiten aufzudecken, gemäß Art. 6 Abs. 1 S. 1 lit.
f&#41;
DSGVO.
</p>
<p>
Im Hinblick auf die Verarbeitung besonderer Kategorien personenbezogener Daten ist diese auf Grundlage des
Hinweisgeberschutzgesetzes aus Gründen eines erheblichen öffentlichen Interesses erforderlich, gemäß Art. 9
Abs. 2 lit. g&#41; DSGVO. Die Verarbeitung dieser besonderen Daten erfolgt gemäß Art. 9 Abs. 2 lit. f&#41;
DSGVO in
Verbindung mit Art. 6 Abs. 1 S. 1 lit. f&#41; DSGVO, um Rechtsansprüche festzustellen, auszuüben oder zu
verteidigen.
</p>
<p>
Betroffene Personen sind diejenigen, über die eine Meldung gemacht wird. Dies können Mitarbeiter,
Vertragspartner oder andere Personen sein, die in beruflicher Verbindung zu der verarbeitenden Stelle
stehen. Darüber hinaus verarbeiten wir personenbezogene Daten der hinweisgebenden Person, wenn diese ihre
Kontaktinformationen oder andere identifizierende Informationen übermittelt. Hinweisgebende Personen sollten
sich daher bewusst sein, dass wir im Rahmen der Bearbeitung des gemeldeten Falls personenbezogene Daten über
sie verarbeiten können.
</p>
<h3>6.2 Kategorien personenbezogener Daten</h3>
<p>
Die Meldung kann anonym erfolgen, wodurch keine personenbezogenen Daten der meldenden Person verarbeitet
werden. Die Art der personenbezogenen Daten, die verarbeitet werden, hängt von den übermittelten
Informationen ab. Wenn die meldende Person personenbezogene Daten über eine andere Person, einschließlich
der gemeldeten Person oder Personen, angibt, werden auch diese Daten verarbeitet. Folgende Kategorien von
personenbezogenen Daten können verarbeitet werden:
</p>
<ul>
<li>Allgemeine personenbezogene Daten (z.B.: Vorname, Nachname, Adresse, E-Mail-Adresse, Telefonnummer,
usw.)</li>
<li>Personenbezogene Daten zu strafrechtlichen Verurteilungen oder Verdachtsmomenten</li>
<li>Besondere Kategorien personenbezogener Daten (Informationen über rassische oder ethnische Herkunft,
politische Meinungen, religiöse oder philosophische Überzeugungen, Gewerkschaftszugehörigkeit,
Gesundheitsdaten sowie Informationen über das Sexualleben oder die sexuelle Orientierung einer Person)
</li>
</ul>
<p>
Wir bitten die meldende Person, ausschließlich Informationen zu übermitteln, die für den jeweiligen Fall von
Bedeutung sind, und insbesondere keine sensiblen Informationen zu melden, es sei denn, diese sind für die
Bearbeitung des gemeldeten Falls von zentraler Relevanz.
</p>
<h3>6.3 Verpflichtung zur Bereitstellung personenbezogener Daten</h3>
<p>
Es ist nicht erforderlich, die im Abschnitt 6.2 genannten personenbezogenen Daten bereitzustellen, da auch
eine anonyme Meldung möglich ist. Bitte beachte jedoch, dass wir möglicherweise nicht in der Lage sind, die
Meldung zu bearbeiten, wenn keine personenbezogenen Daten angegeben werden.
</p>
<h3>6.4 Empfänger personenbezogener Daten</h3>
<p>Die Meldungen werden bei der verarbeitenden Stelle im System als Vorgänge erfasst. Nach einer Bewertung
werden diese Vorgänge intern an die zuständigen Fachabteilungen weitergeleitet, und gegebenenfalls werden
Folgemaßnahmen eingeleitet. Sollte eine Meldung eine der Konzerngesellschaften der verarbeitenden Stelle
betreffen, werden die relevanten Vorgänge an die zuständigen Personen der jeweiligen Gesellschaft
weitergegeben, die dann intern eine Bewertung vornehmen und gegebenenfalls Maßnahmen ergreifen. Bei der
Weitergabe personenbezogener Daten wird der Grundsatz der Datenminimierung beachtet, was bedeutet, dass nur
die unbedingt notwendigen Daten zur Bearbeitung der Meldung weitergegeben werden.</p>
<p>Personenbezogene Daten der hinweisgebenden Person werden an Behörden weitergeleitet, wenn dies erforderlich
ist, um schwerwiegende Verstöße oder Angelegenheiten zu behandeln oder das Recht auf Verteidigung der
betroffenen Personen zu sichern. In anderen Fällen erfolgt die Weitergabe personenbezogener Daten der
hinweisgebenden Person nur mit deren Zustimmung. Daten über andere Personen als die hinweisgebende Person
werden nur im Rahmen der Nachverfolgung eines gemeldeten Falls oder zur Bearbeitung schwerwiegender Verstöße
oder Angelegenheiten weitergegeben.</p>
<p>Die Meldeplattform wird von dem Auftragsverarbeiter WhistleB Whistleblowing Centre AB mit Sitz in Stockholm,
Schweden, bereitgestellt. Weitere Informationen zu WhistleB und den entsprechenden Nutzungsbedingungen sind
dort einsehbar.
<a
href="https://report.whistleb.com/content/documents/whistleb_terms_of_use.pdf">whistleb_terms_of_use.pdf</a>
</p>
<h3>6.5 Speicherdauer</h3>
<p>Personenbezogene Daten, die sich als nicht relevant für die Bearbeitung eines gemeldeten Falls herausstellen,
sowie Meldungen, die wir als unbegründet ansehen, werden umgehend als "nicht relevant" eingestuft. In diesem
Fall wird der Personenbezug entfernt, es sei denn, es handelt sich bereits um eine anonyme Meldung. Um die
gesetzlich vorgeschriebene Dokumentationspflicht und die Löschfristen gemäß § 11 Abs. 1 und Abs. 5 HinSchG
zu erfüllen, wird die Meldung zunächst ohne Personenbezug archiviert, jedoch noch nicht gelöscht.
Archivierte Fälle dienen ausschließlich der Erfüllung dieser Dokumentationspflichten und können danach nicht
mehr zur Bearbeitung herangezogen werden.</p>
<p>Die Meldungen und personenbezogenen Daten, die im Zuge der Bearbeitung einer Meldung erfasst werden, bilden
die Grundlage für die weitere Bearbeitung und werden so schnell wie möglich anonymisiert. Sollte es
notwendig sein, Folgemaßnahmen gemäß §§ 3 Abs. 8 und 18 HinSchG zu ergreifen, kann es jedoch erforderlich
sein, von der Anonymisierung abzuweichen, sei es aufgrund behördlicher Anordnungen oder zur Wahrung von
Rechtsansprüchen. In solchen Fällen wird in der Regel eine Pseudonymisierung angestrebt, es sei denn, es
gibt andere Vorgaben, wie etwa eine richterliche Anordnung. Die Dokumentation wird drei Jahre nach Abschluss
des Verfahrens gelöscht. Sie kann jedoch länger aufbewahrt werden, um den Anforderungen dieses Gesetzes oder
anderer Rechtsvorschriften gerecht zu werden, solange dies notwendig und angemessen ist.</p>
</section>
</body>
</html>

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>
@@ -153,133 +153,6 @@
<a href="https://www.bfdi.bund.de/DE/Service/Anschriften/Laender/Laender-node.html">Laender-node.html</a>
</p>
</section>
<section>
<h2>6. Whistleblower System</h2>
<p>
Compliance with legal regulations and internal guidelines, including our Code of Conduct and the Code of
Conduct for Business Partners, is our (the data processing entity's) top priority. This applies both to our
own business operations and to our supply chains.
</p>
<p>
It is important to us to identify risks early and avoid violations. We aim to take appropriate measures in a
timely manner to prevent potential harm to affected persons, customers, employees, business partners, and
our corporate group.
</p>
<p>
For this reason, we have established an independent, neutral, and confidential whistleblower system that
enables internal and external whistleblowers to submit reports, including anonymously. Through our
transparent complaint procedure, we offer the greatest possible protection, especially to the affected
persons, whistleblowers, and employees involved in investigating reported incidents.
</p>
<p>
Under this procedure, any actual or alleged violations of legal requirements, our Code of Conduct, or the
Code of Conduct for Business Partners may be reported. Human rights or environmental risks, as well as
breaches of duty along the entire supply chain of our group companies and in our own business operations,
can also be the subject of a report.
</p>
<p>
Standardized and swift processes, as well as confidential and professional handling of the reports by
internal experts, form the basis of this fair procedure. Discrimination or punishment of whistleblowers and
individuals responsible for handling complaints and reports will not be tolerated.
</p>
<h3>6.1 Purpose and Legal Basis of Data Processing</h3>
<p>
The purpose of processing personal data is to manage the whistleblower system, which also includes
identifying serious violations or potential violations of applicable law and other serious matters. The
processing of this data is necessary to comply with legal obligations imposed on us, in accordance with Art.
6 para. 1 sentence 1 lit. c) GDPR. This refers to the law that enhances the protection of whistleblowers
(Whistleblower Protection Act - HinSchG).
</p>
<p>
Additionally, the processing serves the legitimate interest of identifying serious violations or potential
violations of applicable law and other serious matters, in accordance with Art. 6 para. 1 sentence 1 lit. f)
GDPR.
</p>
<p>
Regarding the processing of special categories of personal data, this is necessary based on the
Whistleblower Protection Act for reasons of significant public interest, in accordance with Art. 9 para. 2
lit. g) GDPR. The processing of such special data is carried out in accordance with Art. 9 para. 2 lit. f)
GDPR in conjunction with Art. 6 para. 1 sentence 1 lit. f) GDPR to establish, exercise, or defend legal
claims.
</p>
<p>
Affected persons are those about whom a report is made. These can be employees, contractors, or other
individuals in a business relationship with the data processing entity. Furthermore, we process personal
data of the whistleblower if they provide their contact details or other identifying information.
Whistleblowers should be aware that we may process personal data about them during the handling of the
reported case.
</p>
<h3>6.2 Categories of Personal Data</h3>
<p>
Reports can be made anonymously, in which case no personal data of the reporting person will be processed.
The type of personal data processed depends on the information provided. If the reporting person provides
personal data about another individual, including the reported individual or persons, that data will also be
processed. The following categories of personal data may be processed:
</p>
<ul>
<li>General personal data (e.g., first name, last name, address, email address, phone number, etc.)</li>
<li>Personal data related to criminal convictions or suspicions</li>
<li>Special categories of personal data (information about racial or ethnic origin, political opinions,
religious or philosophical beliefs, trade union membership, health data, and information about a
person's sex life or sexual orientation)</li>
</ul>
<p>
We ask the reporting person to only provide information relevant to the case and to avoid reporting
sensitive information unless it is essential for handling the reported case.
</p>
<h3>6.3 Obligation to Provide Personal Data</h3>
<p>
It is not mandatory to provide the personal data mentioned in section 6.2, as anonymous reporting is also
possible. However, please note that we may be unable to process the report if no personal data is provided.
</p>
<h3>6.4 Recipients of Personal Data</h3>
<p>
Reports are logged in the system of the data processing entity as cases. After evaluation, these cases are
forwarded internally to the relevant departments, and follow-up actions may be initiated. If a report
involves one of the group companies of the data processing entity, the relevant cases are forwarded to the
responsible individuals at the respective company, who will then conduct an internal evaluation and take
action if necessary. When transferring personal data, the principle of data minimization is observed,
meaning only the data strictly necessary for handling the report is shared.
</p>
<p>
Personal data of the whistleblower will be shared with authorities when necessary to address serious
violations or issues, or to safeguard the right to defense of the affected persons. In other cases, personal
data of the whistleblower will only be shared with their consent. Data about persons other than the
whistleblower will only be shared in connection with the investigation of a reported case or to address
serious violations or issues.
</p>
<p>
The reporting platform is provided by the processor WhistleB Whistleblowing Centre AB, based in Stockholm,
Sweden. Further information about WhistleB and the corresponding terms of use can be found at:
<a
href="https://report.whistleb.com/content/documents/whistleb_terms_of_use.pdf">whistleb_terms_of_use.pdf</a>
</p>
<h3>6.5 Retention Period</h3>
<p>
Personal data that is found to be irrelevant to the processing of a reported case, as well as reports deemed
unfounded, will be immediately classified as "not relevant." In this case, the personal reference is removed
unless the report was anonymous from the outset. To meet the legally required documentation obligations and
deletion periods pursuant to § 11 para. 1 and para. 5 HinSchG, the report is initially archived without
personal reference but is not yet deleted. Archived cases serve solely to fulfill these documentation
obligations and can no longer be used for further processing.
</p>
<p>
Reports and personal data collected during the processing of a report form the basis for further handling
and are anonymized as soon as possible. However, if it is necessary to take follow-up actions pursuant to §§
3 para. 8 and 18 HinSchG, it may be necessary to deviate from anonymization, whether due to official orders
or to protect legal claims. In such cases, pseudonymization is generally sought, unless other directives
apply, such as a court order. Documentation is deleted three years after the conclusion of the process, but
it may be retained longer if required to meet the requirements of this law or other legal provisions, as
long as it remains necessary and appropriate.
</p>
</section>
</body>
</html>

View File

@@ -25,8 +25,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B29
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Tests.Application", "EnvelopeGenerator.Tests.Application\EnvelopeGenerator.Tests.Application.csproj", "{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastructure", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "presentation", "presentation", "{E3C758DC-914D-4B7E-8457-0813F1FDB0CB}"
@@ -37,6 +35,8 @@ Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.Form", "E
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.PdfEditor", "EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj", "{211619F5-AE25-4BA5-A552-BACAFE0632D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "EnvelopeGenerator.Tests\EnvelopeGenerator.Tests.csproj", "{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -75,10 +75,6 @@ Global
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.Build.0 = Debug|Any CPU
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Release|Any CPU.Build.0 = Release|Any CPU
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -91,6 +87,10 @@ Global
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Release|Any CPU.Build.0 = Release|Any CPU
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -105,12 +105,12 @@ Global
{5A9984F8-51A2-4558-A415-EC5FEED7CF7D} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{E5E12BA4-60C1-48BA-9053-0F8B62B38124} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{9943209E-1744-4944-B1BA-4F87FC1A0EEB} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399} = {0CBC2432-A561-4440-89BC-671B66A24146}
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
{E3C758DC-914D-4B7E-8457-0813F1FDB0CB} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
{A9F9B431-BB9B-49B8-9E2C-0703634A653A} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}