Compare commits

53 Commits

Author SHA1 Message Date
OlgunR
e6b41f10c8 Merge branch 'feat/receiver-ui' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/receiver-ui 2025-12-10 11:16:27 +01:00
OlgunR
dc78ad4a24 Improve canvas mouse position accuracy; add settings.json
Refactored mouse event handling in pdfInterop.js to use a new getPos helper, ensuring accurate coordinate mapping on scaled or resized canvases. Updated start and move functions to use this helper. Added an empty settings.json file.
2025-12-10 11:16:13 +01:00
b282318298 Add launchSettings.json for ReceiverUIBlazor project
Configured development launch profile with browser launch, environment variable, and HTTP/HTTPS URLs for EnvelopeGenerator.ReceiverUIBlazor.
2025-12-10 09:55:32 +01:00
8d68ea8c57 Add IIS publish profile for EnvelopeGenerator.ReceiverUIBlazor
Introduced IISProfile.pubxml to enable packaging and deployment
of the EnvelopeGenerator.ReceiverUIBlazor project to IIS. The
profile configures publish method, build settings, output
package location, and IIS app path.
2025-12-10 09:55:24 +01:00
OlgunR
7cb8b02b1d Revamp UI colors and fonts for warmer, modern look
Switched to "Red Hat Text" and "Teko" fonts via Google Fonts. Updated color palette from cool blues to warm reds and yellows, including backgrounds, accents, and error states. Refreshed button, top bar, and card styles for improved visual hierarchy and softer appearance.
2025-12-09 10:58:53 +01:00
OlgunR
cc1d33462c Add drag-and-drop PDF support to Blazor app
Implement drag-and-drop PDF loading via JS interop and DotNetObjectReference. Refactor file loading logic and UI structure for clarity. Add IAsyncDisposable for resource cleanup. Update pdfInterop.js to handle drop events and send PDF data to Blazor.
2025-12-09 10:42:11 +01:00
OlgunR
cd85b4fffc Improve PDF reset logic and modernize UI styling
Refactor PDF reset to restore original document state using a new OriginalPdfBase64 variable and async logic. Redesign app.css with a lighter color palette, CSS variables, and updated styles for buttons, overlays, modals, and inputs for a cleaner, more accessible UI. Adjust signature and text overlay colors in pdfInterop.js for better contrast and consistency.
2025-12-09 10:26:01 +01:00
OlgunR
ab3e7fb4e9 Error while dragging fields fixed 2025-12-09 09:50:10 +01:00
OlgunR
4a7676765c Fixed error while loading file 2025-12-09 08:57:39 +01:00
OlgunR
562ceb9c3f First results converting receiver-ui-react into a Blazor Web App 2025-12-08 16:21:10 +01:00
OlgunR
751ea706df Created EnvelopeGenerator.ReceiverUIBlazor 2025-12-08 16:06:26 +01:00
OlgunR
490ad9f7cf Created EnvelopeGenerator.ReceiverUIBlazor 2025-12-08 16:04:51 +01:00
7aeaba7c12 Add EnvelopeGenerator.ReceiverUI project and UI components
Introduced a new Razor Components-based web application, `EnvelopeGenerator.ReceiverUI`, to the solution. This includes:

- Added `EnvelopeGenerator.ReceiverUI` project to `EnvelopeGenerator.sln` with Debug and Release configurations.
- Created `EnvelopeGenerator.ReceiverUI.csproj` targeting `.NET 8.0` with nullable and implicit usings enabled.
- Added Razor components (`App.razor`, `Routes.razor`, `MainLayout.razor`, `NavMenu.razor`, `Counter.razor`, `Error.razor`, `Home.razor`, `Weather.razor`) for layout, navigation, and pages.
- Included styles (`MainLayout.razor.css`, `NavMenu.razor.css`, `app.css`) and assets (`favicon.png`, `bootstrap.min.css`).
- Configured logging and allowed hosts in `appsettings.json` and `appsettings.Development.json`.
- Added `Program.cs` to configure and run the application.
- Added `launchSettings.json` for development and debugging profiles.

These changes enable a modern, interactive web interface with Bootstrap styling and development-friendly configurations.
2025-12-05 09:08:57 +01:00
2301a81a1c init receiver ui with react 2025-12-04 18:11:47 +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
e66c46767e refactor: convert markdown parsing to IIFE and preserve indentation 2025-11-18 11:34:37 +01:00
bc732d311c feat: enable async marked parsing and improve markdown rendering
- Configure marked with async, breaks, and GFM options
- Update markdown processing to use textContent and async marked.parse
- Replace synchronous innerHTML parsing with awaited async parsing
2025-11-18 10:00:38 +01:00
c90d29d654 chore: remove IISProfileNetMulti 2025-11-17 16:20:27 +01:00
47a2e950ca chore: add IIS profile for .net 8 2025-11-17 16:19:51 +01:00
6ef989213e chore(Web): add multi-targeting for net7.0, net8.0, and net9.0 with framework-specific package references
Refactored the project file to support multiple target frameworks and added
conditional ItemGroups. Updated several package versions for net8.0 and net9.0
targets to ensure compatibility while keeping net7.0 dependencies unchanged.
2025-11-17 15:23:04 +01:00
2a27b6161b chore(Web): bump to 3.7.0 2025-11-17 12:50:49 +01:00
efdc372b04 feat(envelope): bypass access-code flow when UseAccessCode is disabled
- Add logic to skip access-code workflow if UseAccessCode is false
- Auto-authenticate receiver and show envelope immediately
- Preserve rejection and signed checks
- Keep existing behavior for access-code enabled envelopes
2025-11-17 12:49:15 +01:00
698b7ca1ac feat: render markdown content using marked library 2025-11-15 00:56:19 +01:00
bf6947a28c chore(Web): bump to 3.6.0 2025-11-14 23:05:47 +01:00
e2e31e2e69 feat(ShowEnvelope): render envelope message using Markdown parser 2025-11-14 23:02:37 +01:00
73f6221c3c chore(Web): add marked.js 2025-11-14 15:13:25 +01:00
10f730a833 chore(Web): bump to 3.5.1 2025-11-14 14:31:31 +01:00
cf5a301942 refactor(EnvelopeController): add cancellation token to LogInEnvelope POST action
- Updated `LogInEnvelope` method to accept `CancellationToken` for better async operation handling.
- No functional changes to other methods.
- Maintains backwards compatibility with obsolete MediatR services.
2025-11-14 14:29:25 +01:00
e364f1f592 fix: handle rejected documents correctly with info alert and auto-reload 2025-11-14 14:20:34 +01:00
8a488d4e71 fix: improve finish handler error handling for unavailable envelopes
- Update FINISH flow to handle status codes 409 and 423
- Reload page on 423 responses
- Show proper warning when envelope is no longer available (409)
2025-11-14 14:12:21 +01:00
f0be1a5b03 feat(annotation): update response status codes for signed/rejected checks 2025-11-14 14:08:10 +01:00
773721b634 fix(AnnotationController): enforce proper rejection and history checks in CreateOrUpdate
- Updated `AnyHistoryAsync` call to filter by `EnvelopeRejected` and `DocumentRejected` statuses, returning 403 instead of 200 when applicable.
- Ensures users cannot proceed if envelope was previously rejected.
- Minor cleanup in CreateOrUpdate logic to better handle authorization and signed checks.
2025-11-14 13:49:58 +01:00
99e3e4c24d refactor: update CountHistoryQueryExtensions to use IEnumerable<EnvelopeStatus> and improve status handling 2025-11-14 13:47:34 +01:00
b9c86ce3c6 refactor(CountHistoryQuery): replace single status filter with status range in CountHistoryQuery
- Updated AnyHistoryAsync extension to use EnvelopeStatusQuery instead of single EnvelopeStatus.
- Modified CountHistoryQuery to support multiple statuses (Min, Max, Include, Ignore).
- Preserved backward compatibility with obsolete single Status and EnvelopeId.
2025-11-14 13:39:39 +01:00
637b45efe0 feat(history-query): support multiple status filters in CountHistoryQueryHandler
- Added handling for `Statuses` property to filter by min, max, include, and ignore lists.
- Deprecated single `Status` handling is now wrapped with pragma warnings for backward compatibility.
- Ensures `AnyHistoryAsync` extension works correctly with enhanced query filtering.
2025-11-14 13:36:44 +01:00
28b8c311f9 refactor(CountHistoryQuery): simplify AnyHistoryAsync extension to use UUID and status
- Replace Action-based query configuration with direct parameters (uuid, status)
- Automatically constructs CountHistoryQuery from provided parameters
- Keeps existing EnvelopeId/UUID validation in handler
- Improves readability and usability of history count checks
2025-11-14 13:27:26 +01:00
00c7fe5316 feat(history): enhance ReadHistoryQuery to support Envelope object and UUID
- Replace nullable `OnlyLast` with non-nullable default `true`
- Support filtering by Envelope object (Id or Uuid) in addition to deprecated EnvelopeId
- Throw `BadRequestException` if no valid Envelope reference is provided
- Preserve status filtering and ordering for latest history entries
2025-11-14 13:24:02 +01:00
e5a061d5b5 feat(ReadHistoryQuery): enhance ReadHistoryQuery to support Envelope object and UUID
- Replace nullable `OnlyLast` with non-nullable default `true`
- Support filtering by Envelope object (Id or Uuid) in addition to deprecated EnvelopeId
- Throw `BadRequestException` if no valid Envelope reference is provided
- Preserve status filtering and ordering for latest history entries
2025-11-14 13:19:56 +01:00
629b02863b refactor(history): introduce extension method and remove AutoMapper dependency
- Added `CountHistoryQueryExtensions.AnyHistoryAsync` for cleaner query usage via `ISender`.
- Removed `IMapper` from `CountHistoryQueryHandler` as it was unused.
- Removed unnecessary `using` directives to simplify imports.
- Kept query handling logic intact.
2025-11-14 11:57:16 +01:00
3b24755c35 feat(CountHistoryQuery): add CountHistoryQuery to retrieve envelope history count
- Introduced `CountHistoryQuery` record for querying the count of history entries of an envelope.
- Implemented `CountHistoryQueryHandler` using repository pattern and AutoMapper.
- Supports optional filtering by `Status`.
- Throws `NotFoundException` if necessary (placeholder for future extension).
2025-11-14 11:49:51 +01:00
864e9e8164 refactor(HistoryQueryBase): created to handle basic history queries.
- imp to ReadHistoryQuery
2025-11-14 11:46:15 +01:00
7eff958d0a refactor(ReadHistoryQueryHandler): move to ReadHistoryQuery 2025-11-14 11:33:57 +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
86 changed files with 14884 additions and 436 deletions

1
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -82,7 +82,7 @@ public record EnvelopeDto
/// <summary>
///
/// </summary>
public bool? UseAccessCode { get; set; }
public bool UseAccessCode { get; set; } = true;
/// <summary>
///

View File

@@ -0,0 +1,101 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Histories.Queries;
//TODO: Add sender query
/// <summary>
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
/// </summary>
public record CountHistoryQuery : HistoryQueryBase, IRequest<int>;
/// <summary>
///
/// </summary>
public static class CountHistoryQueryExtensions
{
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="uuid"></param>
/// <param name="statuses"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public static async Task<bool> AnyHistoryAsync(this ISender sender, string uuid, IEnumerable<EnvelopeStatus> statuses, CancellationToken cancel = default)
{
var count = await sender.Send(new CountHistoryQuery
{
Envelope = new() { Uuid = uuid },
Statuses = new() { Include = statuses }
}, cancel);
return count > 0;
}
}
/// <summary>
///
/// </summary>
public class CountHistoryQueryHandler : IRequestHandler<CountHistoryQuery, int>
{
private readonly IRepository<History> _repo;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
public CountHistoryQueryHandler(IRepository<History> repo)
{
_repo = repo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
public Task<int> Handle(CountHistoryQuery request, CancellationToken cancel = default)
{
var query = _repo.Query;
if (request.Envelope.Id is int envId)
query = query.Where(e => e.Id == envId);
else if (request.Envelope.Uuid is string uuid)
query = query.Where(e => e.Envelope!.Uuid == uuid);
#pragma warning disable CS0618 // Type or member is obsolete
else if (request.EnvelopeId is not null)
query = query.Where(h => h.EnvelopeId == request.EnvelopeId);
#pragma warning restore CS0618 // Type or member is obsolete
else
throw new BadRequestException("Invalid request: An Envelope object or a valid EnvelopeId/UUID must be supplied.");
#pragma warning disable CS0618 // Type or member is obsolete
if (request.Status is not null)
query = query.Where(h => h.Status == request.Status);
#pragma warning restore CS0618 // Type or member is obsolete
if (request.Statuses is not null)
{
var status = request.Statuses;
if (status.Min is not null)
query = query.Where(er => er.Envelope!.Status >= status.Min);
if (status.Max is not null)
query = query.Where(er => er.Envelope!.Status <= status.Max);
if (status.Include?.Count() > 0)
query = query.Where(er => status.Include.Contains(er.Envelope!.Status));
if (status.Ignore is not null)
query = query.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
}
return query.CountAsync(cancel);
}
}

View File

@@ -0,0 +1,60 @@
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Constants;
using System.ComponentModel.DataAnnotations;
namespace EnvelopeGenerator.Application.Histories.Queries;
//TODO: Add sender query
/// <summary>
///
/// </summary>
public record HistoryQueryBase
{
/// <summary>
/// Die eindeutige Kennung des Umschlags.
/// </summary>
[Obsolete("Use Envelope property")]
public int? EnvelopeId { get; set; }
/// <summary>
/// Der Include des Umschlags, der abgefragt werden soll. Kann optional angegeben werden, um die Ergebnisse zu filtern.
/// </summary>
[Obsolete("Use statuses")]
public EnvelopeStatus? Status { get; set; }
/// <summary>
///
/// </summary>
public EnvelopeStatusQuery Statuses { get; set; } = new();
/// <summary>
///
/// </summary>
public EnvelopeQueryBase Envelope { get; set; } = new EnvelopeQueryBase();
}
/// <summary>
///
/// </summary>
public record EnvelopeStatusQuery
{
/// <summary>
/// Der minimale Statuswert, der berücksichtigt werden.
/// </summary>
public EnvelopeStatus? Min { get; init; }
/// <summary>
/// Der maximale Statuswert, der berücksichtigt werden.
/// </summary>
public EnvelopeStatus? Max { get; init; }
/// <summary>
/// Eine Liste von Statuswerten, die einbezogen werden.
/// </summary>
public IEnumerable<EnvelopeStatus>? Include { get; init; }
/// <summary>
/// Eine Liste von Statuswerten, die ignoriert werden werden.
/// </summary>
public IEnumerable<EnvelopeStatus>? Ignore { get; init; }
}

View File

@@ -1,7 +1,10 @@
using EnvelopeGenerator.Application.Common.Dto.History;
using EnvelopeGenerator.Domain.Constants;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto.History;
using MediatR;
using System.ComponentModel.DataAnnotations;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Histories.Queries;
@@ -9,21 +12,81 @@ namespace EnvelopeGenerator.Application.Histories.Queries;
/// <summary>
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
/// </summary>
public record ReadHistoryQuery : IRequest<IEnumerable<HistoryDto>>
public record ReadHistoryQuery : HistoryQueryBase, IRequest<IEnumerable<HistoryDto>>
{
/// <summary>
/// Die eindeutige Kennung des Umschlags.
/// </summary>
[Required]
public int EnvelopeId { get; init; }
/// <summary>
/// Der Include des Umschlags, der abgefragt werden soll. Kann optional angegeben werden, um die Ergebnisse zu filtern.
/// </summary>
public EnvelopeStatus? Status { get; init; }
/// <summary>
/// Abfrage zur Steuerung, ob nur der aktuelle Include oder der gesamte Datensatz zurückgegeben wird.
/// </summary>
public bool? OnlyLast { get; init; } = true;
public bool OnlyLast { get; init; } = true;
}
/// <summary>
///
/// </summary>
public class ReadHistoryQueryHandler : IRequestHandler<ReadHistoryQuery, IEnumerable<HistoryDto>>
{
private readonly IRepository<History> _repo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
/// <param name="mapper"></param>
public ReadHistoryQueryHandler(IRepository<History> repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
public async Task<IEnumerable<HistoryDto>> Handle(ReadHistoryQuery request, CancellationToken cancel = default)
{
var query = _repo.Query;
if (request.Envelope.Id is int envId)
query = query.Where(e => e.Id == envId);
else if (request.Envelope.Uuid is string uuid)
query = query.Where(e => e.Envelope!.Uuid == uuid);
#pragma warning disable CS0618 // Type or member is obsolete
else if (request.EnvelopeId is not null)
query = query.Where(h => h.EnvelopeId == request.EnvelopeId);
#pragma warning restore CS0618 // Type or member is obsolete
else
throw new BadRequestException("Invalid request: An Envelope object or a valid EnvelopeId/UUID must be supplied.");
#pragma warning disable CS0618 // Type or member is obsolete
if (request.Status is not null)
query = query.Where(h => h.Status == request.Status);
#pragma warning restore CS0618 // Type or member is obsolete
if (request.Statuses is not null)
{
var status = request.Statuses;
if (status.Min is not null)
query = query.Where(er => er.Envelope!.Status >= status.Min);
if (status.Max is not null)
query = query.Where(er => er.Envelope!.Status <= status.Max);
if (status.Include?.Count() > 0)
query = query.Where(er => status.Include.Contains(er.Envelope!.Status));
if (status.Ignore is not null)
query = query.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
}
if (request.OnlyLast)
query = query.OrderByDescending(x => x.AddedWhen);
var hists = await query.ToListAsync(cancel);
return _mapper.Map<List<HistoryDto>>(hists);
}
}

View File

@@ -1,47 +0,0 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto.History;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Histories.Queries;
/// <summary>
///
/// </summary>
public class ReadHistoryQueryHandler : IRequestHandler<ReadHistoryQuery, IEnumerable<HistoryDto>>
{
private readonly IRepository<History> _repo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
/// <param name="mapper"></param>
public ReadHistoryQueryHandler(IRepository<History> repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
public async Task<IEnumerable<HistoryDto>> Handle(ReadHistoryQuery request, CancellationToken cancel = default)
{
var query = _repo.ReadOnly().Where(h => h.EnvelopeId == request.EnvelopeId);
if (request.Status is not null)
query = query.Where(h => h.Status == request.Status);
var hists = await query.ToListAsync(cancel);
return _mapper.Map<List<HistoryDto>>(hists);
}
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="EnvelopeGenerator.ReceiverUI.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

View File

@@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -0,0 +1,30 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">EnvelopeGenerator.ReceiverUI</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>

View File

@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,19 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@@ -0,0 +1,64 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUI
@using EnvelopeGenerator.ReceiverUI.Components

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,27 @@
using EnvelopeGenerator.ReceiverUI.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();

View File

@@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:26087",
"sslPort": 44331
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7124;http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,51 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,13 @@
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>EnvelopeGenerator.ReceiverUIBlazor</AssemblyName>
<RootNamespace>EnvelopeGenerator.ReceiverUIBlazor</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,428 @@
@page "/"
@using Microsoft.JSInterop
@inject IJSRuntime JS
@implements IAsyncDisposable
<div class="page-shell">
<h1>Sign PDF (Blazor)</h1>
<div class="controls">
<InputFile OnChange="HandleFileSelected" accept="application/pdf" />
<button class="btn" @onclick="ShowSignaturePad" disabled="@(!HasPdf)">Add signature</button>
<button class="btn" @onclick="() => ShowTextOverlay(false)" disabled="@(!HasPdf)">Add text</button>
<button class="btn" @onclick="() => ShowTextOverlay(true)" disabled="@(!HasPdf)">Add date</button>
<button class="btn" @onclick="Reset" disabled="@(!HasPdf)">Reset</button>
<button class="btn secondary" @onclick="Download" disabled="@(!HasPdf)">Download</button>
</div>
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<div class="error-banner">@ErrorMessage</div>
}
@if (!HasPdf)
{
<div class="drop-hint">Drop or select a PDF to start.</div>
}
@if (HasPdf)
{
<div class="document-shell" @ref="PdfHostRef" style="@($"width:{ViewportWidthPx}px")">
<canvas id="pdf-canvas" @ref="PdfCanvasRef"></canvas>
@if (ShowSignature)
{
<div class="overlay signature" @ref="OverlayRef"
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px; width:{OverlayWidthPx}px; height:{OverlayHeightPx}px;")"
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
<div class="overlay-controls">
<button class="overlay-btn" @onclick="ApplySignature" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
</div>
<img src="@SignatureDataUrl" draggable="false" />
</div>
}
@if (ShowText)
{
<div class="overlay text" @ref="OverlayRef"
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px;")"
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
<div class="overlay-controls">
<button class="overlay-btn" @onclick="ApplyText" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
</div>
<input class="overlay-input" @bind="TextValue" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation />
</div>
}
</div>
<div class="paging">
<button class="btn" @onclick="PrevPage" disabled="@(!CanPrev)">&lt;</button>
<span>Page @DisplayPage / @PageCount</span>
<button class="btn" @onclick="NextPage" disabled="@(!CanNext)">&gt;</button>
</div>
}
</div>
@if (ShowSignaturePadModal)
{
<div class="modal-backdrop">
<div class="modal">
<h3>Add signature</h3>
<canvas id="@SignatureCanvasId" width="700" height="220"></canvas>
<div class="modal-row">
<label><input type="checkbox" @bind="AutoDate" /> Auto date/time</label>
<button class="btn" @onclick="ClearSignature">Clear</button>
<span class="spacer"></span>
<button class="btn" @onclick="ConfirmSignature">Use signature</button>
<button class="btn secondary" @onclick="CloseSignaturePad">Cancel</button>
</div>
</div>
</div>
}
@code {
private ElementReference PdfCanvasRef;
private ElementReference PdfHostRef;
private ElementReference OverlayRef;
private string? PdfBase64;
private string? OriginalPdfBase64;
private DotNetObjectReference<Index>? _dotNetRef;
private int PageIndex;
private int PageCount;
private double ViewportWidthPx = 800;
private double ViewportHeightPx;
private bool ShowSignaturePadModal;
private bool ShowSignature;
private bool ShowText;
private string SignatureCanvasId { get; } = $"sig-{Guid.NewGuid():N}";
private string? SignatureDataUrl;
private bool AutoDate = true;
private double OverlayXpx = 20;
private double OverlayYpx = 20;
private double OverlayWidthPx = 200;
private double OverlayHeightPx = 80;
private bool IsDragging;
private double DragStartX;
private double DragStartY;
private double StartLeft;
private double StartTop;
private string TextValue = "Text";
private string? ErrorMessage;
private DateTimeOffset _lastDragRender = DateTimeOffset.MinValue;
private bool HasPdf => !string.IsNullOrWhiteSpace(PdfBase64);
private int DisplayPage => PageIndex + 1;
private bool CanPrev => PageIndex > 0;
private bool CanNext => PageIndex + 1 < PageCount;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
_dotNetRef ??= DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("pdfInterop.registerDropHandler", _dotNetRef);
}
if (ShowSignaturePadModal)
{
await JS.InvokeVoidAsync("pdfInterop.initSignaturePad", SignatureCanvasId);
}
}
private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
if (e.FileCount == 0)
{
return;
}
await LoadPdfFromBrowserFile(e.File);
}
private async Task LoadPdfFromBrowserFile(IBrowserFile file)
{
ErrorMessage = null;
try
{
await using var stream = file.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
await LoadPdfFromBase64Internal(Convert.ToBase64String(ms.ToArray()));
}
catch (Exception ex)
{
ErrorMessage = $"Fehler beim Laden der PDF: {ex.Message}";
PdfBase64 = null;
PageCount = 0;
PageIndex = 0;
}
}
[JSInvokable]
public Task LoadPdfFromBase64(string base64)
{
return LoadPdfFromBase64Internal(base64);
}
private async Task LoadPdfFromBase64Internal(string base64)
{
ErrorMessage = null;
PdfBase64 = base64;
OriginalPdfBase64 = PdfBase64;
// Show the canvas before we start rendering
await InvokeAsync(StateHasChanged);
await Task.Yield();
// Make sure pdf.js is ready
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
PageCount = result.Pages;
PageIndex = 0;
await RenderPage();
}
private async Task RenderPage()
{
if (!HasPdf)
{
return;
}
try
{
var viewport = await JS.InvokeAsync<ViewportInfo>("pdfInterop.renderPage", PageIndex, "pdf-canvas", ViewportWidthPx);
ViewportWidthPx = viewport.Width;
ViewportHeightPx = viewport.Height;
StateHasChanged();
}
catch (Exception ex)
{
ErrorMessage = $"Fehler beim Rendern: {ex.Message}";
}
}
private async Task Reset()
{
ErrorMessage = null;
CloseOverlays();
ShowSignaturePadModal = false;
OverlayXpx = 20;
OverlayYpx = 20;
OverlayWidthPx = 200;
OverlayHeightPx = 80;
TextValue = "Text";
if (string.IsNullOrWhiteSpace(OriginalPdfBase64))
{
return;
}
PdfBase64 = OriginalPdfBase64;
PageIndex = 0;
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
PageCount = result.Pages;
await RenderPage();
}
private void CloseOverlays()
{
ShowSignature = false;
ShowText = false;
SignatureDataUrl = null;
}
private void ShowSignaturePad()
{
ShowSignaturePadModal = true;
}
private async Task ConfirmSignature()
{
SignatureDataUrl = await JS.InvokeAsync<string>("pdfInterop.getSignatureDataUrl", SignatureCanvasId);
if (string.IsNullOrWhiteSpace(SignatureDataUrl))
{
return;
}
OverlayWidthPx = 200;
OverlayHeightPx = 80;
OverlayXpx = 20;
OverlayYpx = 20;
ShowSignature = true;
ShowText = false;
ShowSignaturePadModal = false;
}
private void CloseSignaturePad()
{
ShowSignaturePadModal = false;
}
private void ClearSignature()
{
JS.InvokeVoidAsync("pdfInterop.clearSignaturePad", SignatureCanvasId);
}
private void ShowTextOverlay(bool autoDate)
{
TextValue = autoDate ? DateTimeOffset.Now.ToString("M/d/yyyy HH:mm:ss zzz") : "Text";
OverlayWidthPx = 240;
OverlayHeightPx = 40;
OverlayXpx = 20;
OverlayYpx = 20;
ShowText = true;
ShowSignature = false;
}
private void StartDrag(PointerEventArgs args)
{
IsDragging = true;
DragStartX = args.ClientX;
DragStartY = args.ClientY;
StartLeft = OverlayXpx;
StartTop = OverlayYpx;
if (OverlayRef.Context != null)
{
JS.InvokeVoidAsync("pdfInterop.capturePointer", OverlayRef, args.PointerId);
}
}
private void OnDrag(PointerEventArgs args)
{
if (!IsDragging)
{
return;
}
var dx = args.ClientX - DragStartX;
var dy = args.ClientY - DragStartY;
OverlayXpx = StartLeft + dx;
OverlayYpx = StartTop + dy;
var now = DateTimeOffset.UtcNow;
if (now - _lastDragRender > TimeSpan.FromMilliseconds(16))
{
_lastDragRender = now;
InvokeAsync(StateHasChanged);
}
}
private void EndDrag(PointerEventArgs args)
{
IsDragging = false;
if (OverlayRef.Context != null)
{
JS.InvokeVoidAsync("pdfInterop.releasePointer", OverlayRef, args.PointerId);
}
}
private async Task ApplySignature()
{
if (SignatureDataUrl is null || !HasPdf)
{
return;
}
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applySignature", new
{
base64 = PdfBase64,
pageIndex = PageIndex,
left = OverlayXpx,
top = OverlayYpx,
width = OverlayWidthPx,
height = OverlayHeightPx,
renderWidth = ViewportWidthPx,
renderHeight = ViewportHeightPx,
dataUrl = SignatureDataUrl,
autoDate = AutoDate,
});
CloseOverlays();
await RenderPage();
}
private async Task ApplyText()
{
if (string.IsNullOrWhiteSpace(TextValue) || !HasPdf)
{
return;
}
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applyText", new
{
base64 = PdfBase64,
pageIndex = PageIndex,
left = OverlayXpx,
top = OverlayYpx,
width = OverlayWidthPx,
height = OverlayHeightPx,
renderWidth = ViewportWidthPx,
renderHeight = ViewportHeightPx,
text = TextValue,
fontSize = 20,
});
CloseOverlays();
await RenderPage();
}
private void CancelOverlay()
{
CloseOverlays();
}
private async Task PrevPage()
{
if (!CanPrev)
{
return;
}
PageIndex--;
await RenderPage();
}
private async Task NextPage()
{
if (!CanNext)
{
return;
}
PageIndex++;
await RenderPage();
}
private async Task Download()
{
if (!HasPdf)
{
return;
}
await JS.InvokeVoidAsync("pdfInterop.downloadPdf", PdfBase64, "document-signed.pdf");
}
private record RenderResult(int Pages);
private record ViewportInfo(double Width, double Height, double PageWidth, double PageHeight);
public async ValueTask DisposeAsync()
{
_dotNetRef?.Dispose();
await Task.CompletedTask;
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using EnvelopeGenerator.ReceiverUIBlazor;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<WebPublishMethod>Package</WebPublishMethod>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>7f262ad4-53b1-42d3-9a5f-132cf50f150c</ProjectGuid>
<DesktopBuildPackageLocation>E:\TekH\Visual Studio\src\EnvelopeGenerator.ReceiverUIBlazor\EnvelopeGenerator.ReceiverUIBlazor.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>ReceiverUIBlazor</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"EnvelopeGenerator.ReceiverUIBlazor": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:49582;http://localhost:49583"
}
}
}

View File

@@ -0,0 +1,10 @@
@inherits LayoutComponentBase
<div class="main-layout">
<header class="top-bar">
<div class="brand">Receiver UI (Blazor)</div>
</header>
<main class="content">
@Body
</main>
</div>

View File

@@ -0,0 +1,8 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUIBlazor
@using EnvelopeGenerator.ReceiverUIBlazor.Shared

View File

@@ -0,0 +1,241 @@
@import url('https://fonts.googleapis.com/css2?family=Red+Hat+Text:wght@400;500;600;700&family=Teko:wght@500;600&display=swap');
:root {
--bg: #f7f7f8;
--bg-strong: #fff6f6;
--text: #474747;
--muted: #777777;
--border: #e7e7e7;
--shadow: 0 18px 55px rgba(20, 20, 20, 0.08);
--card: #ffffff;
--accent: #a52431;
--accent-strong: #8d1e2a;
--accent-soft: #f8e5e8;
--highlight: #ffd62f;
--danger: #a52431;
}
body {
margin: 0;
font-family: "Red Hat Text", "Segoe UI", system-ui, -apple-system, sans-serif;
background: radial-gradient(120% 120% at 6% 12%, var(--bg-strong) 0%, #fffdf7 45%, var(--bg) 85%);
color: var(--text);
line-height: 1.5;
}
.main-layout {
min-height: 100vh;
}
.top-bar {
display: flex;
align-items: center;
padding: 14px 24px;
background: var(--accent);
border-bottom: 1px solid var(--accent-strong);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
color: #ffd62f;
}
.top-bar .brand {
font-family: "Teko", "Red Hat Text", sans-serif;
font-weight: 600;
letter-spacing: 0.6px;
color: #ffd62f;
}
.content {
padding: 28px 32px 40px;
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
h1 {
margin: 0 0 10px;
letter-spacing: 0.2px;
font-family: "Teko", "Red Hat Text", sans-serif;
font-weight: 600;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 14px;
justify-content: center;
}
.btn {
background: #4a4a4a;
color: #ffffff;
border: 1px solid #404040;
padding: 10px 15px;
border-radius: 12px;
font-weight: 700;
cursor: pointer;
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
}
.btn:hover:not(:disabled) {
transform: translateY(-1px);
background: #3f3f3f;
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
box-shadow: none;
}
.btn.secondary {
background: #f4f4f4;
color: #474747;
border: 1px solid #d3d3d3;
box-shadow: none;
}
.btn.secondary:hover:not(:disabled) {
background: #e9e9e9;
transform: translateY(-1px);
}
.drop-hint {
padding: 26px;
border: 1px dashed var(--border);
border-radius: 14px;
text-align: center;
color: var(--muted);
background: #ffffff;
width: min(1100px, 100%);
margin: 0 auto;
box-shadow: 0 8px 18px rgba(165, 36, 49, 0.06);
}
.error-banner {
margin-top: 10px;
padding: 12px 14px;
border-radius: 10px;
background: #fcebec;
border: 1px solid #f3c6cd;
color: var(--accent-strong);
}
.document-shell {
position: relative;
margin-top: 14px;
border-radius: 14px;
overflow: hidden;
box-shadow: var(--shadow);
background: var(--card);
border: 1px solid var(--border);
margin-left: auto;
margin-right: auto;
}
canvas {
display: block;
width: 100%;
height: auto;
background: #ffffff;
}
.overlay {
position: absolute;
border: 1px dashed var(--accent);
border-radius: 10px;
padding: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 12px 30px rgba(165, 36, 49, 0.15);
user-select: none;
touch-action: none;
}
.overlay.signature img {
display: block;
max-width: 100%;
max-height: 100%;
}
.overlay-controls {
position: absolute;
top: -44px;
right: 0;
display: flex;
gap: 6px;
}
.overlay-btn {
background: #ffffff;
color: var(--accent-strong);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 6px 10px;
cursor: pointer;
box-shadow: 0 6px 16px rgba(165, 36, 49, 0.16);
}
.overlay-input {
border: 1px solid var(--border);
background: #ffffff;
color: var(--text);
font-size: 18px;
padding: 6px 8px;
min-width: 180px;
border-radius: 8px;
outline: none;
}
.paging {
margin-top: 14px;
display: flex;
gap: 10px;
align-items: center;
color: var(--muted);
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.35);
backdrop-filter: blur(3px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 18px;
min-width: 760px;
box-shadow: var(--shadow);
}
.modal-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 12px;
color: var(--muted);
}
.modal canvas {
background: #ffffff;
border: 1px solid var(--border);
border-radius: 10px;
}
.spacer {
flex: 1;
}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Receiver UI (Blazor)</title>
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<!-- pdf.js 3.11 UMD + classic worker for compatibility; SRI removed to avoid digest mismatches -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"></script>
<script src="js/pdfInterop.js"></script>
</head>
<body>
<div id="app">Loading...</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@@ -0,0 +1,339 @@
(function () {
// Stick to pdf.js 3.11 UMD + classic worker for compatibility.
const PDF_JS_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
const WORKER_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
const state = {
pdfDoc: null,
pdfBytes: null,
lastViewport: null,
pdfJsReady: null,
};
function base64ToUint8(base64) {
const binStr = atob(base64);
const len = binStr.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binStr.charCodeAt(i);
}
return bytes;
}
async function reloadFromBase64(base64) {
state.pdfBytes = base64ToUint8(base64);
state.pdfDoc = await pdfjsLib.getDocument({ data: state.pdfBytes }).promise;
return { pages: state.pdfDoc.numPages };
}
function dataUrlDownload(dataUrl, filename) {
const a = document.createElement('a');
a.href = dataUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
const pointerPads = new Map();
function loadScriptOnce(url) {
return new Promise((resolve, reject) => {
// If already present, resolve immediately
const existing = Array.from(document.getElementsByTagName('script')).find(s => s.src === url);
if (existing && existing.dataset.loaded === "true") {
resolve();
return;
}
const script = existing || document.createElement('script');
script.src = url;
script.defer = true;
script.onload = () => {
script.dataset.loaded = "true";
resolve();
};
script.onerror = (e) => reject(new Error(`Script load failed: ${url}`));
if (!existing) {
document.head.appendChild(script);
}
});
}
async function ensurePdfJsLoaded() {
if (typeof pdfjsLib !== "undefined") {
return;
}
if (!state.pdfJsReady) {
state.pdfJsReady = loadScriptOnce(PDF_JS_SRC);
}
await state.pdfJsReady;
if (typeof pdfjsLib === "undefined") {
throw new Error("pdfjsLib could not be loaded");
}
}
window.pdfInterop = {
ensureReady: async () => {
// Ensure pdf.js is present and the worker path is set explicitly.
await ensurePdfJsLoaded();
if (pdfjsLib && pdfjsLib.GlobalWorkerOptions) {
if (pdfjsLib.GlobalWorkerOptions.workerSrc !== WORKER_SRC) {
pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_SRC;
}
} else {
throw new Error("pdf.js not available after load");
}
},
loadPdf: async (base64) => {
await ensurePdfJsLoaded();
try {
const result = await reloadFromBase64(base64);
if (!result || !result.pages) {
throw new Error("PDF has keine Seiten erkannt");
}
return result;
} catch (err) {
console.error("pdfInterop.loadPdf failed", err);
throw err;
}
},
renderPage: async (pageIndex, canvasId, targetWidth) => {
await ensurePdfJsLoaded();
if (!state.pdfDoc) {
throw new Error('PDF not loaded');
}
const page = await state.pdfDoc.getPage(pageIndex + 1);
const rawViewport = page.getViewport({ scale: 1 });
const scale = targetWidth / rawViewport.width;
const viewport = page.getViewport({ scale });
let canvas = document.getElementById(canvasId);
if (!canvas) {
// give the UI a tiny delay to render the canvas into the DOM
await new Promise(r => setTimeout(r, 40));
canvas = document.getElementById(canvasId);
}
if (!canvas) {
console.error("renderPage: canvas not found", canvasId);
throw new Error('Canvas not found');
}
const ctx = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
state.lastViewport = {
width: viewport.width,
height: viewport.height,
pageWidth: rawViewport.width,
pageHeight: rawViewport.height,
};
return state.lastViewport;
},
applySignature: async (payload) => {
const {
base64,
pageIndex,
left,
top,
width,
height,
renderWidth,
renderHeight,
dataUrl,
autoDate,
} = payload;
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
const page = pdfDoc.getPage(pageIndex);
const scaleX = page.getWidth() / renderWidth;
const scaleY = page.getHeight() / renderHeight;
const pngImage = await pdfDoc.embedPng(dataUrl);
const drawWidth = width * scaleX;
const drawHeight = height * scaleY;
const x = left * scaleX;
const y = page.getHeight() - (top + height) * scaleY;
page.drawImage(pngImage, {
x,
y,
width: drawWidth,
height: drawHeight,
});
if (autoDate) {
const text = `Signed ${new Date().toLocaleString()}`;
page.drawText(text, {
x,
y: y - 14 * scaleY,
size: 14 * scaleX,
color: PDFLib.rgb(0.11, 0.25, 0.56),
});
}
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
await reloadFromBase64(updatedBase64);
return updatedBase64;
},
applyText: async (payload) => {
const {
base64,
pageIndex,
left,
top,
width,
height,
renderWidth,
renderHeight,
text,
fontSize,
} = payload;
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
const page = pdfDoc.getPage(pageIndex);
const scaleX = page.getWidth() / renderWidth;
const scaleY = page.getHeight() / renderHeight;
const x = left * scaleX;
const y = page.getHeight() - (top + height) * scaleY;
page.drawText(text, {
x,
y,
size: fontSize * scaleX,
color: PDFLib.rgb(0.2, 0.23, 0.28),
});
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
await reloadFromBase64(updatedBase64);
return updatedBase64;
},
downloadPdf: (base64, filename) => {
dataUrlDownload(`data:application/pdf;base64,${base64}`, filename);
},
initSignaturePad: (canvasId) => {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.strokeStyle = '#1c3d8f';
const padState = {
drawing: false,
lastX: 0,
lastY: 0,
};
function getPos(evt) {
const rect = canvas.getBoundingClientRect();
const scaleX = rect.width ? canvas.width / rect.width : 1;
const scaleY = rect.height ? canvas.height / rect.height : 1;
return {
x: (evt.clientX - rect.left) * scaleX,
y: (evt.clientY - rect.top) * scaleY,
};
}
function start(e) {
padState.drawing = true;
const pos = getPos(e);
padState.lastX = pos.x;
padState.lastY = pos.y;
}
function move(e) {
if (!padState.drawing) return;
const pos = getPos(e);
const x = pos.x;
const y = pos.y;
ctx.beginPath();
ctx.moveTo(padState.lastX, padState.lastY);
ctx.lineTo(x, y);
ctx.stroke();
padState.lastX = x;
padState.lastY = y;
}
function end() {
padState.drawing = false;
}
canvas.onpointerdown = start;
canvas.onpointermove = move;
canvas.onpointerup = end;
canvas.onpointerleave = end;
pointerPads.set(canvasId, { ctx, canvas });
},
registerDropHandler: (dotNetRef) => {
if (window.__pdfDropRegistered) return;
window.__pdfDropRegistered = true;
const prevent = (e) => {
e.preventDefault();
e.stopPropagation();
};
['dragenter', 'dragover', 'dragleave'].forEach(evt => {
document.addEventListener(evt, prevent, false);
});
document.addEventListener('drop', (e) => {
prevent(e);
const files = e.dataTransfer?.files;
if (!files || files.length === 0) {
return;
}
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
if (typeof result === 'string') {
const base64 = result.split(',')[1] || result;
dotNetRef?.invokeMethodAsync('LoadPdfFromBase64', base64);
}
};
reader.readAsDataURL(file);
}, false);
},
clearSignaturePad: (canvasId) => {
const pad = pointerPads.get(canvasId);
if (!pad) return;
pad.ctx.clearRect(0, 0, pad.canvas.width, pad.canvas.height);
},
getSignatureDataUrl: (canvasId) => {
const pad = pointerPads.get(canvasId);
if (!pad) return null;
return pad.canvas.toDataURL('image/png');
},
capturePointer: (element, pointerId) => {
if (element && element.setPointerCapture) {
try {
element.setPointerCapture(pointerId);
} catch (err) {
console.warn('capturePointer failed', err);
}
}
},
releasePointer: (element, pointerId) => {
if (element && element.releasePointerCapture) {
try {
element.releasePointerCapture(pointerId);
} catch (err) {
console.warn('releasePointer failed', err);
}
}
}
};
})();

View File

@@ -0,0 +1,52 @@
{
"homepage": "https://slavik0329.github.io/pdf-sign",
"name": "pdf-sign",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"annotpdf": "^1.0.12",
"dayjs": "^1.9.6",
"pdf-lib": "^1.12.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-draggable": "^4.4.3",
"react-dropzone": "^11.2.4",
"react-icons": "^4.1.0",
"react-pdf": "^5.0.0",
"react-scripts": "4.0.1",
"react-signature-canvas": "^1.0.3",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"gh-pages": "^3.1.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Finally, a simple way to put your signature on a PDF for free."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>PDF-sign</title>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,10 @@
body {
font-family: "Open Sans";
}
input:focus,
select:focus,
textarea:focus,
button:focus {
outline: none;
}

View File

@@ -0,0 +1,278 @@
import "./App.css";
import { useRef, useState } from "react";
import Drop from "./Drop";
import { Document, Page, pdfjs } from "react-pdf";
import { PDFDocument, rgb } from "pdf-lib";
import { blobToURL } from "./utils/Utils";
import PagingControl from "./components/PagingControl";
import { AddSigDialog } from "./components/AddSigDialog";
import { Header } from "./Header";
import { BigButton } from "./components/BigButton";
import DraggableSignature from "./components/DraggableSignature";
import DraggableText from "./components/DraggableText";
import dayjs from "dayjs";
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
function downloadURI(uri, name) {
var link = document.createElement("a");
link.download = name;
link.href = uri;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function App() {
const styles = {
container: {
maxWidth: 900,
margin: "0 auto",
},
sigBlock: {
display: "inline-block",
border: "1px solid #000",
},
documentBlock: {
maxWidth: 800,
margin: "20px auto",
marginTop: 8,
border: "1px solid #999",
},
controls: {
maxWidth: 800,
margin: "0 auto",
marginTop: 8,
},
};
const [pdf, setPdf] = useState(null);
const [autoDate, setAutoDate] = useState(true);
const [signatureURL, setSignatureURL] = useState(null);
const [position, setPosition] = useState(null);
const [signatureDialogVisible, setSignatureDialogVisible] = useState(false);
const [textInputVisible, setTextInputVisible] = useState(false);
const [pageNum, setPageNum] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [pageDetails, setPageDetails] = useState(null);
const documentRef = useRef(null);
return (
<div>
<Header />
<div style={styles.container}>
{signatureDialogVisible ? (
<AddSigDialog
autoDate={autoDate}
setAutoDate={setAutoDate}
onClose={() => setSignatureDialogVisible(false)}
onConfirm={(url) => {
setSignatureURL(url);
setSignatureDialogVisible(false);
}}
/>
) : null}
{!pdf ? (
<Drop
onLoaded={async (files) => {
const URL = await blobToURL(files[0]);
setPdf(URL);
}}
/>
) : null}
{pdf ? (
<div>
<div style={styles.controls}>
{!signatureURL ? (
<BigButton
marginRight={8}
title={"Add signature"}
onClick={() => setSignatureDialogVisible(true)}
/>
) : null}
<BigButton
marginRight={8}
title={"Add Date"}
onClick={() => setTextInputVisible("date")}
/>
<BigButton
marginRight={8}
title={"Add Text"}
onClick={() => setTextInputVisible(true)}
/>
<BigButton
marginRight={8}
title={"Reset"}
onClick={() => {
setTextInputVisible(false);
setSignatureDialogVisible(false);
setSignatureURL(null);
setPdf(null);
setTotalPages(0);
setPageNum(0);
setPageDetails(null);
}}
/>
{pdf ? (
<BigButton
marginRight={8}
inverted={true}
title={"Download"}
onClick={() => {
downloadURI(pdf, "file.pdf");
}}
/>
) : null}
</div>
<div ref={documentRef} style={styles.documentBlock}>
{textInputVisible ? (
<DraggableText
initialText={
textInputVisible === "date"
? dayjs().format("M/d/YYYY")
: null
}
onCancel={() => setTextInputVisible(false)}
onEnd={setPosition}
onSet={async (text) => {
const { originalHeight, originalWidth } = pageDetails;
const scale = originalWidth / documentRef.current.clientWidth;
const y =
documentRef.current.clientHeight -
(position.y +
(12 * scale) -
position.offsetY -
documentRef.current.offsetTop);
const x =
position.x -
166 -
position.offsetX -
documentRef.current.offsetLeft;
// new XY in relation to actual document size
const newY =
(y * originalHeight) / documentRef.current.clientHeight;
const newX =
(x * originalWidth) / documentRef.current.clientWidth;
const pdfDoc = await PDFDocument.load(pdf);
const pages = pdfDoc.getPages();
const firstPage = pages[pageNum];
firstPage.drawText(text, {
x: newX,
y: newY,
size: 20 * scale,
});
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)]);
const URL = await blobToURL(blob);
setPdf(URL);
setPosition(null);
setTextInputVisible(false);
}}
/>
) : null}
{signatureURL ? (
<DraggableSignature
url={signatureURL}
onCancel={() => {
setSignatureURL(null);
}}
onSet={async () => {
const { originalHeight, originalWidth } = pageDetails;
const scale = originalWidth / documentRef.current.clientWidth;
const y =
documentRef.current.clientHeight -
(position.y -
position.offsetY +
64 -
documentRef.current.offsetTop);
const x =
position.x -
160 -
position.offsetX -
documentRef.current.offsetLeft;
// new XY in relation to actual document size
const newY =
(y * originalHeight) / documentRef.current.clientHeight;
const newX =
(x * originalWidth) / documentRef.current.clientWidth;
const pdfDoc = await PDFDocument.load(pdf);
const pages = pdfDoc.getPages();
const firstPage = pages[pageNum];
const pngImage = await pdfDoc.embedPng(signatureURL);
const pngDims = pngImage.scale( scale * .3);
firstPage.drawImage(pngImage, {
x: newX,
y: newY,
width: pngDims.width,
height: pngDims.height,
});
if (autoDate) {
firstPage.drawText(
`Signed ${dayjs().format(
"M/d/YYYY HH:mm:ss ZZ"
)}`,
{
x: newX,
y: newY - 10,
size: 14 * scale,
color: rgb(0.074, 0.545, 0.262),
}
);
}
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)]);
const URL = await blobToURL(blob);
setPdf(URL);
setPosition(null);
setSignatureURL(null);
}}
onEnd={setPosition}
/>
) : null}
<Document
file={pdf}
onLoadSuccess={(data) => {
setTotalPages(data.numPages);
}}
>
<Page
pageNumber={pageNum + 1}
width={800}
height={1200}
onLoadSuccess={(data) => {
setPageDetails(data);
}}
/>
</Document>
</div>
<PagingControl
pageNum={pageNum}
setPageNum={setPageNum}
totalPages={totalPages}
/>
</div>
) : null}
</div>
</div>
);
}
export default App;

View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,37 @@
import React, { useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { cleanBorder, primary45 } from "./utils/colors";
export default function Drop({ onLoaded }) {
const styles = {
container: {
textAlign: "center",
border: cleanBorder,
padding: 20,
marginTop: 12,
color: primary45,
fontSize: 18,
fontWeight: 600,
borderRadius: 4,
userSelect: "none",
outline: 0,
cursor: "pointer",
},
};
const onDrop = useCallback((acceptedFiles) => {
onLoaded(acceptedFiles);
// Do something with the files
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: "application/pdf",
});
return (
<div {...getRootProps()} style={styles.container}>
<input {...getInputProps()} />
{isDragActive ? <p>Drop a PDF here</p> : <p>Drag a PDF here</p>}
</div>
);
}

View File

@@ -0,0 +1,15 @@
import {primary45} from "./utils/colors";
export function Header() {
const styles = {
container: {
backgroundColor: primary45,
color: '#FFF',
padding: 12,
fontWeight: 600,
}
}
return <div style={styles.container}>
<div>Open PDF Sign</div>
</div>
}

View File

@@ -0,0 +1,77 @@
import { Dialog } from "./Dialog";
import SignatureCanvas from "react-signature-canvas";
import { ConfirmOrCancel } from "./ConfirmOrCancel";
import { primary45 } from "../utils/colors";
import { useRef } from "react";
export function AddSigDialog({ onConfirm, onClose, autoDate, setAutoDate }) {
const sigRef = useRef(null);
const styles = {
sigContainer: {
display: "flex",
justifyContent: "center",
},
sigBlock: {
display: "inline-block",
border: `1px solid ${primary45}`,
},
instructions: {
display: "flex",
justifyContent: "space-between",
textAlign: "center",
color: primary45,
marginTop: 8,
width: 600,
alignSelf: "center",
},
instructionsContainer: {
display: "flex",
justifyContent: "center",
},
};
return (
<Dialog
isVisible={true}
title={"Add signature"}
body={
<div style={styles.container}>
<div style={styles.sigContainer}>
<div style={styles.sigBlock}>
<SignatureCanvas
velocityFilterWeight={1}
ref={sigRef}
canvasProps={{
width: "600",
height: 200,
className: "sigCanvas",
}}
/>
</div>
</div>
<div style={styles.instructionsContainer}>
<div style={styles.instructions}>
<div>
Auto date/time{" "}
<input
type={"checkbox"}
checked={autoDate}
onChange={(e) => setAutoDate(e.target.checked)}
/>
</div>
<div>Draw your signature above</div>
</div>
</div>
<ConfirmOrCancel
onCancel={onClose}
onConfirm={() => {
const sigURL = sigRef.current.toDataURL();
onConfirm(sigURL);
}}
/>
</div>
}
/>
);
}

View File

@@ -0,0 +1,81 @@
import React from "react";
import { primary45 } from "../utils/colors";
import useHover from "../hooks/useHover";
export function BigButton({
title,
onClick,
inverted,
fullWidth,
customFillColor,
customWhiteColor,
style,
noHover,
id,
small,
disabled,
marginRight,
}) {
const [hoverRef, isHovered] = useHover();
let fillColor = customFillColor || primary45;
const whiteColor = customWhiteColor || "#FFF";
let initialBg = null;
let hoverBg = fillColor;
let initialColor = fillColor;
let hoverColor = whiteColor;
if (inverted) {
initialBg = fillColor;
hoverBg = null;
initialColor = whiteColor;
hoverColor = fillColor;
}
if (disabled) {
initialBg = "#ddd";
hoverBg = "#ddd";
fillColor = "#ddd";
}
const styles = {
container: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: fullWidth ? "100%" : null,
backgroundColor: isHovered && !noHover ? hoverBg : initialBg,
color:
isHovered && !noHover && !disabled
? hoverColor
: disabled
? "#999"
: initialColor,
borderRadius: 4,
padding: small ? "2px 4px" : "6px 8px",
fontSize: small ? 14 : null,
border: `1px solid ${fillColor}`,
cursor: !disabled ? "pointer" : null,
userSelect: "none",
boxSizing: "border-box",
marginRight,
},
};
return (
<div
id={id}
ref={hoverRef}
style={{ ...styles.container, ...style }}
onClick={() => {
if (!disabled) {
onClick();
}
}}
>
{title}
</div>
);
}

View File

@@ -0,0 +1,37 @@
import { BigButton } from "./BigButton";
import React from "react";
export function ConfirmOrCancel({
onCancel,
onConfirm,
confirmTitle = "Confirm",
leftBlock,
hideCancel,
disabled
}) {
const styles = {
actions: {
display: "flex",
justifyContent: "space-between",
},
cancel: {
marginRight: 8,
},
};
return (
<div style={styles.actions}>
<div>{leftBlock}</div>
<div>
{!hideCancel ? (
<BigButton
title={"Cancel"}
style={styles.cancel}
onClick={onCancel}
/>
) : null}
<BigButton title={confirmTitle} inverted={true} onClick={onConfirm} disabled={disabled}/>
</div>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import {primary45} from '../utils/colors';
import {FaTimes} from 'react-icons/fa';
import {Modal} from './Modal';
export function Dialog({
isVisible,
body,
onClose,
title,
noPadding,
backgroundColor,
positionTop,
style,
}) {
if (!isVisible) {
return null;
}
const styles = {
header: {
backgroundColor: primary45,
color: '#FFF',
padding: 8,
fontSize: 14,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
body: {
padding: noPadding ? 0 : 14,
backgroundColor: backgroundColor ? backgroundColor : '#FFF',
},
xIcon: {
cursor: 'pointer',
},
};
return (
<Modal onClose={onClose} isVisible={isVisible} positionTop={positionTop} style={style}>
<div style={styles.container}>
<div style={styles.header}>
<div>{title}</div>
<FaTimes
color={'#FFF'}
size={16}
style={styles.xIcon}
className={'dialogClose'}
onClick={onClose}
/>
</div>
<div style={styles.body}>{body}</div>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,37 @@
import Draggable from "react-draggable";
import {BigButton} from "./BigButton"; // The default
import {FaCheck, FaTimes} from 'react-icons/fa'
import {cleanBorder, errorColor, goodColor, primary45} from "../utils/colors";
export default function DraggableSignature({ url, onEnd, onSet, onCancel }) {
const styles = {
container: {
position: 'absolute',
zIndex: 100000,
border: `2px solid ${primary45}`,
},
controls: {
position: 'absolute',
right: 0,
display: 'inline-block',
backgroundColor: primary45,
// borderRadius: 4,
},
smallButton: {
display: 'inline-block',
cursor: 'pointer',
padding: 4,
}
}
return (
<Draggable onStop={onEnd}>
<div style={styles.container}>
<div style={styles.controls}>
<div style={styles.smallButton} onClick={onSet}><FaCheck color={goodColor}/></div>
<div style={styles.smallButton} onClick={onCancel}><FaTimes color={errorColor}/></div>
</div>
<img src={url} width={200} style={styles.img} draggable={false} />
</div>
</Draggable>
);
}

View File

@@ -0,0 +1,66 @@
import Draggable from "react-draggable";
import { FaCheck, FaTimes } from "react-icons/fa";
import { cleanBorder, errorColor, goodColor, primary45 } from "../utils/colors";
import { useState, useEffect, useRef } from "react";
export default function DraggableText({ onEnd, onSet, onCancel, initialText }) {
const [text, setText] = useState("Text");
const inputRef = useRef(null);
useEffect(() => {
if (initialText) {
setText(initialText)
} else {
inputRef.current.focus();
inputRef.current.select()
}
}, [])
const styles = {
container: {
position: "absolute",
zIndex: 100000,
border: `2px solid ${primary45}`,
},
controls: {
position: "absolute",
right: 0,
display: "inline-block",
backgroundColor: primary45,
// borderRadius: 4,
},
smallButton: {
display: "inline-block",
cursor: "pointer",
padding: 4,
},
input: {
border: 0,
fontSize: 20,
padding: 3,
backgroundColor: 'rgba(0,0,0,0)',
cursor: 'move'
}
};
return (
<Draggable onStop={onEnd}>
<div style={styles.container}>
<div style={styles.controls}>
<div style={styles.smallButton} onClick={()=>onSet(text)}>
<FaCheck color={goodColor} />
</div>
<div style={styles.smallButton} onClick={onCancel}>
<FaTimes color={errorColor} />
</div>
</div>
<input
ref={inputRef}
style={styles.input}
value={text}
placeholder={'Text'}
onChange={(e) => setText(e.target.value)}
/>
</div>
</Draggable>
);
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import {primary45} from '../utils/colors';
import {useIsSmallScreen} from '../hooks/useIsSmallScreen';
export function Modal({onClose, children, isVisible, style, positionTop}) {
const isSmallScreen = useIsSmallScreen();
const styles = {
container: {
position: isSmallScreen ? 'fixed' : 'absolute',
backgroundColor: '#FFF',
border: `1px solid ${primary45}`,
borderRadius: 4,
top: positionTop ? positionTop : isSmallScreen ? 60 : 150,
left: '50%',
transform: 'translateX(-50%)',
width: '94%',
fontFamily: 'Open Sans',
zIndex: 10000,
boxShadow: '0 0px 14px hsla(0, 0%, 0%, 0.2)',
},
background: {
position: 'fixed',
width: '100%',
height: '100%',
top: 0,
left: 0,
backgroundColor: '#00000033',
zIndex: 5000,
},
};
if (!isVisible) {
return null;
}
return (
<div style={styles.outer}>
<div style={styles.background} onClick={onClose} />
<div style={{...styles.container, ...style}}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { BigButton } from "./BigButton";
import {primary45} from "../utils/colors";
export default function PagingControl({totalPages, pageNum, setPageNum}) {
const styles= {
container: {
marginTop: 8,
marginBottom: 8,
},
inlineFlex: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
pageInfo: {
padding: 8,
color: primary45,
fontSize: 14,
}
}
return (
<div style={styles.container}>
<div style={styles.inlineFlex}>
<BigButton
title={"<"}
onClick={() => setPageNum(pageNum - 1)}
disabled={pageNum-1===-1}
/>
<div style={styles.pageInfo}>
Page: {pageNum + 1}/{totalPages}
</div>
<BigButton
title={">"}
onClick={() => setPageNum(pageNum + 1)}
disabled={pageNum+1>totalPages-1}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import React, {useCallback, useRef, useState} from 'react';
export default function useHover() {
const [value, setValue] = useState(false);
const handleMouseOver = useCallback(() => setValue(true), []);
const handleMouseOut = useCallback(() => setValue(false), []);
const ref = useRef();
const callbackRef = useCallback(
(node) => {
if (ref.current) {
ref.current.removeEventListener('mouseenter', handleMouseOver);
ref.current.removeEventListener('mouseleave', handleMouseOut);
}
ref.current = node;
if (ref.current) {
ref.current.addEventListener('mouseenter', handleMouseOver);
ref.current.addEventListener('mouseleave', handleMouseOut);
}
},
[handleMouseOver, handleMouseOut],
);
return [callbackRef, value];
}

View File

@@ -0,0 +1,6 @@
import {useWindowSize} from './useWindowSize';
export function useIsSmallScreen() {
const windowSize = useWindowSize();
return windowSize.width < 600;
}

View File

@@ -0,0 +1,29 @@
import React, {useState, useEffect} from 'react';
export function useWindowSize() {
const isClient = typeof window === 'object';
function getSize() {
return {
width: isClient ? window.innerWidth : undefined,
height: isClient ? window.innerHeight : undefined,
};
}
const [windowSize, setWindowSize] = useState(getSize);
useEffect(() => {
if (!isClient) {
return false;
}
function handleResize() {
setWindowSize(getSize());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures that effect is only run on mount and unmount
return windowSize;
}

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -0,0 +1,27 @@
export function blobToURL(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
const base64data = reader.result;
resolve(base64data);
};
});
}
export async function fileToBlob(file, handleUpdate) {
const { content, size } = file;
let chunks = [];
let i = 0;
const totalCount = Math.round(size / 250000);
for await (const chunk of content) {
if (handleUpdate) {
handleUpdate(i, totalCount);
}
chunks.push(chunk);
i++;
}
// eslint-disable-next-line no-undef
return new Blob(chunks);
}

View File

@@ -0,0 +1,28 @@
export const primary = '#2b6284';
export const primary2 = '#ecf4f9';
export const primary3 = '#9fc7e0';
export const primary35 = '#97bace';
export const primary4 = 'hsl(204,38%,55%)';
export const primary45 = 'hsl(218,49%,66%)';
export const primary46 = '#6778cb';
export const primary15 = 'rgb(241 249 255)';
export const primary5 = '#3881ad';
export const primary6 = '#132b3a';
// export const primary = '#666';
// export const primary2 = '#EEE';
// export const primary3 = '#CCC';
// export const primary4 = '#AAA';
// export const primary5 = '#888';
// export const primary6 = '#333';
export const primary16 = 'hsl(208 100% 96% / 1)';
export const errorColor = '#ef6565';
export const lightErrorColor = '#ef9c9c';
export const goodColor = '#53c171';
export const cleanBorder = '1px solid rgb(208, 227, 239)';
export const lightBorder = 'hsl(203 51% 80% / 1)';

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Web.Extensions;
using MediatR;
@@ -11,7 +12,6 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Dynamic;
namespace EnvelopeGenerator.Web.Controllers;
@@ -58,7 +58,9 @@ public class AnnotationController : ControllerBase
// Again check if receiver has already signed
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
return Problem(statusCode: 403);
return Problem(statusCode: 409);
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
return Problem(statusCode: 423);
var docSignedNotification = await _mediator
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
@@ -71,7 +73,7 @@ public class AnnotationController : ControllerBase
return Ok();
}
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost("reject")]
[Obsolete("Use DigitalData.Core.Exceptions and .Middleware")]

View File

@@ -95,6 +95,23 @@ public class EnvelopeController : ViewControllerBase
}
#endregion
#region UseAccessCode
if (!er.Envelope!.UseAccessCode)
{
(string? uuid, string? signature) = decoded.ParseEnvelopeReceiverId();
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid!, signature: signature!);
if (er_secret_res.IsFailed)
{
_logger.LogNotice(er_secret_res.Notices);
return this.ViewEnvelopeNotFound();
}
var er_secret = er_secret_res.Data;
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.FullyAuth);
return await CreateShowEnvelopeView(er_secret);
}
#endregion UseAccessCode
#region Send Access Code
bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress);
if (!accessCodeAlreadyRequested)
@@ -121,7 +138,7 @@ public class EnvelopeController : ViewControllerBase
[HttpPost("{envelopeReceiverId}")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth)
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth, CancellationToken cancel)
{
try
{
@@ -145,6 +162,15 @@ public class EnvelopeController : ViewControllerBase
}
var er_secret = er_secret_res.Data;
//check rejection
var rejRcvrs = await _historyService.ReadRejectingReceivers(er_secret.Envelope!.Id);
if (rejRcvrs.Any())
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
ViewBag.IsExt = !rejRcvrs.Contains(er_secret.Receiver); //external if the current user is not rejected
return View("EnvelopeRejected", er_secret);
}
// show envelope if already logged in
if (User.IsInRole(ReceiverRole.FullyAuth))
return await CreateShowEnvelopeView(er_secret);
@@ -190,7 +216,7 @@ public class EnvelopeController : ViewControllerBase
return this.ViewInnerServiceError();
}
}
private async Task<IActionResult> CreateEnvelopeLockedView(EnvelopeReceiverDto er, CancellationToken cancel)
{
var uuidClaim = User.GetAuthEnvelopeUuid();

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

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>EnvelopeGenerator.Web</PackageId>
@@ -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.5.0</Version>
<AssemblyVersion>3.5.0</AssemblyVersion>
<FileVersion>3.5.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>
@@ -2094,20 +2094,13 @@
<None Include="wwwroot\lib\bootstrap-icons\icons\zoom-out.svg" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.20" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.5" />
@@ -2126,6 +2119,56 @@
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
<PackageReference Include="Quartz" Version="3.8.0" />
<PackageReference Include="Quartz.AspNetCore" Version="3.8.0" />
<PackageReference Include="Quartz.Plugins" Version="3.8.0" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.8.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="7.0.0" />
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
<PackageReference Include="System.Drawing.Common" Version="8.0.16" />
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
<PackageReference Include="Quartz" Version="3.8.0" />
<PackageReference Include="Quartz.AspNetCore" Version="3.8.0" />
<PackageReference Include="Quartz.Plugins" Version="3.8.0" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.8.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.11" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="7.0.0" />
<PackageReference Include="System.DirectoryServices" Version="9.0.4" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.4" />
<PackageReference Include="System.DirectoryServices.Protocols" Version="9.0.4" />
<PackageReference Include="System.Drawing.Common" Version="9.0.5" />
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />

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

@@ -11,12 +11,10 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\signFLOW\Web\net9\win64\$(Version)\EnvelopeGenerator.Web.zip</DesktopBuildPackageLocation>
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\signFLOW\Web\net8\$(Version)\EnvelopeGenerator.Web.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -79,7 +79,7 @@
}
else
{
<h6>@($"{@envelope?.Message}")</h6>
<div class="markdown">@(envelope?.Message)</div>
}
<p>
<small class="text-body-secondary">

View File

@@ -49,6 +49,7 @@
<script src="~/lib/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="~/lib/sweetalert2/sweetalert2.min.js"></script>
<script src="~/lib/alertifyjs/alertify.min.js"></script>
<script src="~/lib/marked/marked.umd.min.js"></script>
<script src="~/js/lazy.min.js" asp-append-version="true"></script>
<script src="~/js/ui.min.js" asp-append-version="true"></script>
<script src="~/js/annotation.js" asp-append-version="true"></script>
@@ -95,5 +96,6 @@
</div>
<a href="/privacy-policy.@(_localizer.Culture()).html" target="_blank">@_localizer.Privacy()</a>
</footer>
<script src="~/js/markdown.min.js" asp-append-version="true"></script>
</body>
</html>

View File

@@ -41,6 +41,12 @@
"wwwroot/js/util.js"
]
},
{
"outputFileName": "wwwroot/js/markdown.min.js",
"inputFiles": [
"wwwroot/js/markdown.js"
]
},
{
"outputFileName": "wwwroot/css/error-space.min.css",
"inputFiles": [

View File

@@ -255,7 +255,7 @@ class App {
});
if (!res.ok) {
if (res.status === 403) {
if (res.status === 409) {
Swal.fire({
title: 'Warnung',
text: 'Umschlag ist nicht mehr verfügbar.',
@@ -263,6 +263,17 @@ class App {
})
return false
}
else if (res.status === 423) {
Swal.fire({
title: 'Info',
text: 'Dokument wurde von einem Empfänger abgelehnt. Sie werden weitergeleitet...',
icon: 'info',
timer: 2000,
showConfirmButton: false
}).then(() => {
location.reload();
});
}
else {
throw new Error()
}

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(`
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===403)return Swal.fire({title:"Warnung",text:"Umschlag ist nicht mehr verfügbar.",icon:"warning"}),!1;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;}
`)}},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

@@ -0,0 +1,11 @@
marked.use({
async: true,
breaks: true,
gfm: true,
});
(async () => {
for (const el of document.querySelectorAll('.markdown')) {
el.innerHTML = await marked.parse(el.textContent.replace(/(\t| )/g, "&emsp;"));
}
})();

View File

@@ -0,0 +1 @@
marked.use({"async":!0,breaks:!0,gfm:!0});(async()=>{for(const n of document.querySelectorAll(".markdown"))n.innerHTML=await marked.parse(n.textContent.replace(/(\t| )/g,"&emsp;"))})();

File diff suppressed because one or more lines are too long

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

@@ -37,6 +37,10 @@ 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.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{34AF8679-E8AF-4AB6-B861-53ED241B1228}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUIBlazor", "EnvelopeGenerator.ReceiverUIBlazor\EnvelopeGenerator.ReceiverUIBlazor.csproj", "{7F262AD4-53B1-42D3-9A5F-132CF50F150C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -91,6 +95,14 @@ 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
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Debug|Any CPU.Build.0 = Debug|Any CPU
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Release|Any CPU.ActiveCfg = Release|Any CPU
{34AF8679-E8AF-4AB6-B861-53ED241B1228}.Release|Any CPU.Build.0 = Release|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F262AD4-53B1-42D3-9A5F-132CF50F150C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -111,6 +123,8 @@ Global
{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}
{34AF8679-E8AF-4AB6-B861-53ED241B1228} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{7F262AD4-53B1-42D3-9A5F-132CF50F150C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}