Compare commits

...

33 Commits

Author SHA1 Message Date
63c60a91b2 Add multi-targeting support for net7.0 and net8.0
Updated the project to support both net7.0 and net8.0 frameworks.
Modified `<TargetFramework>` to `<TargetFrameworks>` and added
conditional `<ItemGroup>` entries for dependencies and project
references specific to each framework. Updated package versions
(e.g., Microsoft.Extensions.DependencyInjection to 9.0.6) and
introduced new dependencies for net8.0, including
Microsoft.EntityFrameworkCore 8.0.17. Added a `<Compile Remove>`
directive for net7.0 to exclude Crypt-related files. Ensured
compatibility and maintained backward support for net7.0.
2026-10-08 10:33:11 +02:00
a5d2c477f6 Add EnvelopeGenerator.Infrastructure.Crypt project
A new project, `EnvelopeGenerator.Infrastructure.Crypt`, has been added to the solution. This project targets `.NET 8.0` and includes several `Microsoft.Extensions` dependencies (e.g., `Configuration.Binder`, `DependencyInjection.Abstractions`, etc.) and the `Pkcs11Interop` package.

The solution file (`EnvelopeGenerator.sln`) has been updated to include the new project, its build configurations, and its nesting under the same parent as `EnvelopeGenerator.Infrastructure.Doc`.
2026-10-08 10:32:48 +02:00
3a71d57619 Enhance FES docs, plans, and SoftHSM integration
Updated README.md for clarity and added official references. Introduced detailed infrastructure and integration plans for FES, including phased implementation, deployment options, and risk mitigations. Added FES workflow diagrams in draw.io and PDF formats. Documented SoftHSM testing process and integration details in ticket.md. Improved alignment with clean architecture principles and production readiness.
2026-10-08 10:31:00 +02:00
73b6c2712c Update ResourceManager references in translation functions
Updated the `TranslateCertificationType` and `TranslateFinalEmailType` functions to use the `ResourceManager` from the `CommonServices.My.Resources.Model` namespace instead of the `Resources.Model` namespace for consistency and improved resource management.
2026-10-08 10:16:22 +02:00
200972c370 Update README with PKCS#11 integration details
Added a detailed README for the `EnvelopeGenerator.Infrastructure.Crypt` project, explaining its purpose, functionality, and integration with PKCS#11-based cryptographic workflows.

Highlights include:
- Overview of the project's role in FES workflows.
- Explanation of SoftHSM and PKCS#11 deployment patterns.
- Mapping of functionality to FES diagram steps 6-8.
- Documentation of service interfaces and DI entry point.
- Configuration guidance with JSON examples and security best practices.
- Instructions for obtaining PKCS#11 values and setting up SoftHSM.
- Checklist for IT handover and environment setup.
- Steps for running cryptographic tests with PowerShell and Linux examples.
- Common failure diagnostics and their meanings.
- Clarification of signing semantics and validation requirements.
- FAQ addressing common questions about SoftHSM and PKCS#11 usage.

This update ensures clear guidance for developers and IT teams to configure and integrate the cryptographic layer effectively.
2026-10-07 13:35:50 +02:00
9f7519941a Improve bootstrap handling in SenderEditorPage
Added a `DxLoadingPanel` to display a loading indicator during the bootstrap process. Introduced the `IsBootstrapLoading` property to track bootstrap progress. Refactored `OnParametersSetAsync` to handle bootstrap tokens more effectively, including redirecting to `/sender` if no token is provided. Updated `TryApplyBootstrapAsync` to adjust navigation behavior when the bootstrap payload is `null`.

Enhanced `OnAfterRenderAsync` to process `_pendingBootstrapToken` more cleanly and removed redundant logic. Simplified navigation and improved code readability and maintainability.
2026-10-06 13:59:39 +02:00
66e58b4a1a Add envelope settings step to wizard
Introduced a new step (step 2) in the `CreateEnvelopeWizardPopup` for configuring envelope settings, including security, reminders, and expiration options.

- Added fields for certification type, final email options, 2FA, access codes, and reminder settings.
- Updated step titles, descriptions, and total steps from 4 to 5.
- Implemented validation logic for the new settings step.
- Added methods to handle changes in settings fields and apply defaults.
- Updated `CreateEnvelopeWizardResult` and `CreateEnvelopeBootstrapPayload` to include new fields.
- Modified `SenderEditorPage` and `SenderPage` to handle new settings.
- Styled the new step with updated CSS for grids, groups, and toggles.
- Refactored state reset logic to initialize new fields.
2026-10-06 12:59:55 +02:00
de9d8c6f37 Add document preview feature to wizard popup
Introduced a document preview feature in the `CreateEnvelopeWizardPopup` component. This includes:

- Injected `PdfRenderService` for rendering PDF pages.
- Replaced document metadata display with a preview card.
- Added `SenderPdfImageEditor` for interactive PDF previews.
- Implemented state management for preview pages and overlays.
- Added fallback logic to estimate PDF page count.
- Enhanced `ResetWizardState` to reset preview-related fields.
- Styled the preview card and wrap with new CSS classes.
- Added placeholder event handlers for editor interactions.
2026-10-06 11:48:03 +02:00
47f48ba12a Add PdfFileDropZone for interactive PDF uploads
Replaced static upload message in `CreateEnvelopeWizardPopup.razor` with a new `PdfFileDropZone` component to enhance user experience. The component supports drag-and-drop functionality, file selection, and dynamic states (e.g., disabled, drag-active).

Introduced `PdfFileDropZone.razor` with customizable parameters such as `Disabled`, `HasDocument`, `UploadText`, and `DragDropHintText`. Added event handling for drag-and-drop interactions and file selection.

Updated `sender-page.css` to include styles for the new component, ensuring seamless integration with the existing layout. Adjusted styles for hover effects, drag-active state, and disabled state.

These changes improve usability and make PDF uploads more intuitive.
2026-10-06 10:31:43 +02:00
60241c9c6d Refactor receiver suggestions filtering logic
Introduced `ReceiverSuggestionFilterHandler` to refine the
filtering of recent receiver suggestions based on name and
email. Added `SuggestionLimit` constant to replace hardcoded
limits and ensure consistency. Updated UI to use the new
`FilteredRecentSuggestions` property, which leverages the
`BuildFilteredRecentSuggestions` method for grouping,
ordering, and limiting suggestions. Adjusted
`OnDocumentSelectedAsync` to use the new constant. Added
necessary `@using` directive for the filter handler.
2026-10-06 09:26:43 +02:00
2a939b4cad Add filtering logic for receiver suggestions
Introduce `ReceiverFilterCriteria` to encapsulate filtering criteria for receivers, including normalization of `FullName` and `EmailAddress`. Add `ReceiverSuggestionFilterHandler` to filter `RecentReceiverSuggestionDto` objects based on the criteria. Includes methods for matching criteria and filtering suggestions.
2026-10-06 09:26:22 +02:00
e89d471c6f Refactor receiver input and improve UI/UX
Replaced receiver list with a tag-based `DxTagBox` for better flexibility and dynamic updates. Updated receiver management logic to synchronize `_receivers` and `_receiverTags`. Enhanced accessibility with ARIA attributes for the suggestion list.

Redesigned the "Add Receiver" button with custom styles, hover effects, and an inline SVG icon. Improved the suggestion list with horizontal scrolling and nowrap styling. Styled tags and added a remove button for each tag.

Increased recent receiver suggestions fetch limit to 20. Added validation to ensure default envelope type selection. Refactored empty state handling and cleaned up redundant code for better maintainability.
2026-10-05 21:58:51 +02:00
ece676fbcf Make Take optional and refactor query handling logic
Updated the `Take` property in `ReadRecentReceiverSuggestionsQuery` to be nullable, allowing for unlimited results when not specified. Refactored the `Handle` method in `ReadRecentReceiverSuggestionsQueryHandler` to conditionally apply the `Take` limit only when provided. Renamed the cancellation token parameter for consistency. Improved code clarity and flexibility by removing redundant operations and dynamically applying the `Take` logic.
2026-10-05 17:06:47 +02:00
fac4ec28a4 Add localization and UI updates for receiver actions
Added new localized strings for "Recently used recipients" and
"Skip" in German, English, and French resource files. Updated
the `CreateEnvelopeWizardPopup.razor` to enhance the "Add
Receiver" button with a primary render style and a new CSS
class. Introduced styles for the button in `sender-page.css`
to improve its appearance with a minimum width, bold font,
and box shadow.
2026-10-05 17:02:12 +02:00
a3b11ed63f Add recent receiver suggestions to wizard
Introduced a feature to display and apply recent receiver
suggestions in the `CreateEnvelopeWizardPopup` component.
Added a new section in step 2 to show recently used receivers,
with loading and state management logic. Updated the
`ReceiverService` to fetch suggestions from the backend API.

Enhanced the UI with new styles for the suggestions section
and improved localization support. Refactored state
management to handle loading, resetting, and applying
suggestions. Minor cleanup and adjustments to ensure proper
state transitions.
2026-10-05 16:53:57 +02:00
5627483589 Make RecentReceiverSuggestionDto immutable; refactor query
Updated RecentReceiverSuggestionDto to use `init` properties,
adding immutability and a new `LastUsedAt` field. Refactored
ReadRecentReceiverSuggestionsQueryHandler to simplify query
logic, remove `IMapper` dependency, and improve determinism
in results. Introduced tie-breaking for duplicate timestamps
and optimized ordering by `LastUsedAt` and `EmailAddress`.
Removed redundant `Include` and mapping steps for clarity.
2026-10-05 16:43:34 +02:00
bf23e88cf8 Add plan for recent receiver suggestions feature
Added a detailed plan document outlining the development of a "recent receiver suggestions" feature for step 3 of the sender wizard. The plan defines team working rules, objectives, functional requirements, and architecture direction across application, server, and client layers.

The implementation is divided into six phases, covering MediatR query creation, API endpoint addition, client integration, UI updates, regression testing, and technical debt tracking. Each phase includes black-box test suggestions to ensure quality and robustness. The plan emphasizes incremental development, user approval, and quality-focused processes.
2026-10-05 15:58:58 +02:00
9435561e78 Add recent receiver suggestions query and DTO
Introduce `RecentReceiverSuggestionDto` to represent recent receiver suggestions with `FullName` and `EmailAddress`.

Add `ReadRecentReceiverSuggestionsQuery` to retrieve recent receiver suggestions scoped by sender username and limited to 3-5 results.

Implement `ReadRecentReceiverSuggestionsQueryHandler` to process the query using Entity Framework Core and AutoMapper.

Update `MappingProfile` to map `EnvelopeReceiver` to `RecentReceiverSuggestionDto`.
2026-10-05 15:55:19 +02:00
db1e91a3f6 Set C# language version to latest in project file
Added the `<LangVersion>` property to `EnvelopeGenerator.Application.csproj` with the value `latest`. This ensures the project uses the most up-to-date C# language features supported by the compiler, enabling access to new enhancements and features as they become available.
2026-10-05 15:32:41 +02:00
0c664c0995 Improve UX, security, and maintainability
- Introduced a DevExpress toast-based notification system using `DxToastProvider` and `IToastNotificationService` for consistent feedback.
- Standardized request-to-domain mapping with AutoMapper/profile conventions or reflection-based mappers, enforced at the Core library level.
- Removed dynamic SQL string composition and migrated to parameterized queries to address SQL injection vulnerabilities. Added CI guardrails to prevent reintroduction.
- Refined UX on `EnvelopeSenderPage.razor` to enforce deterministic row double-click behavior and align row action buttons/tooltips.
- Enabled SPA-style language switching for immediate UI culture updates without page reloads.
- Added sender-scoped caching for "recent receiver suggestions" with short TTL, invalidation hooks, and cross-sender isolation.
2026-10-05 15:20:58 +02:00
5db2f6f1e3 Refactor envelope type selection logic
Removed the placeholder option from the envelope type dropdown to simplify the UI. Introduced a new `GetDefaultEnvelopeTypeId()` method to centralize and enhance the logic for determining the default envelope type. This method prioritizes envelope types with titles containing "Handwritten Signature" if available, improving alignment with user preferences. Updated `_selectedEnvelopeTypeId` initialization to use the new method for better maintainability.
2026-10-05 15:04:09 +02:00
a535e20b0e Add "Next" button translations to localization files
Added a new resource entry for the "Next" button in the
localization files for German (de-DE), English (en-US), and
French (fr-FR). Translations include "Weiter" in German,
"Next" in English, and "Suivant" in French.
2026-10-05 15:01:15 +02:00
bb35a68b1f feat(sender-editor): consume bootstrap payload and auto-save draft
Accept bootstrapToken query parameter and consume one-time payload from sessionStorage-backed service.

Hydrate editor state with title, message, envelope type, receivers, and PDF document from bootstrap data.

Refactor draft save into reusable core flow and trigger non-navigating auto-save after bootstrap is applied.

Prevent stale token replay by clearing query token from URL and handling invalid tokens gracefully.
2026-10-05 14:54:06 +02:00
715add7f51 feat(sender): add create-envelope wizard with bootstrap handoff
Introduce a 4-step modal wizard on SenderPage for creating envelopes.

Validate required fields, support optional skip, receiver management, PDF upload, and keyboard shortcuts.

Persist wizard payload in sessionStorage via token-based bootstrap service and navigate to editor with bootstrapToken.

Register bootstrap storage service in both client and server DI containers and add dedicated sender wizard styling.
2026-10-05 14:53:48 +02:00
621e22701a Add markdown audit and update architecture docs
Added a "Markdown Audit Snapshot" section to document the status, gaps, and recommendations for markdown files in the repository. Included a "Documentation Audit TODO" checklist for tracking updates. Updated the "Active Architecture (Post-Migration)" section to reflect the current Blazor Auto (Server+WASM hybrid) setup. Identified and marked outdated or redundant markdown files for removal or archival. Highlighted the need for periodic synchronization of documentation with implementation progress.
2026-10-05 09:57:56 +02:00
5ef32a2b0c Adjust SenderEditor UI defaults and accordion styling
Changed the `DxAccordionItem` in `SenderEditorPage.razor` to be collapsed by default. Removed unused elements (`sender-settings-chip` and `sender-settings-grid`) from the accordion's content template.

Updated `envelope-viewer.css` to refine the accordion header's appearance: reduced font size, font weight, and padding, and added a `line-height` property for better spacing.
2026-10-02 15:23:57 +02:00
54d16eb3b8 Refactor SenderEditorPage for modernized UI design
Replaced inline styles with reusable CSS classes to improve maintainability, readability, and scalability. Updated the layout and styling of key components, including the action bar, title and message inputs, PDF metadata display, and buttons. Introduced a `DxAccordion` for envelope settings and redesigned the empty state and save result dialog. Added dynamic styling using CSS variables and centralized styles in `envelope-viewer.css`. Removed redundant inline styles and improved overall UI consistency.
2026-10-02 14:59:43 +02:00
ae3264253d Update ILogger type in SenderPage.razor
Changed the `ILogger` dependency injection type parameter from `EnvelopeSenderPage` to `SenderPage` to ensure alignment with the correct class name or intended logging context.
2026-10-02 14:49:38 +02:00
4cbfe52d5b Add localized strings and update localization logic
Added new localized strings to `Model.en.resx`, `Model.fr-FR.resx`,
and `Model.resx` for additional features/settings in English,
French, and German. Updated `SenderEditorPage.razor` to inject
`ModelLocalizer` and modified the `L` function to prioritize
localized strings from `ModelLocalizer` over `UiLocalizer`.
Included `@using EnvelopeGenerator.Domain.Resources` to ensure
access to the `Model` resource file. These changes improve
localization flexibility and maintainability.
2026-10-02 14:46:40 +02:00
27c6b017cd refactor (EnvelopeSenderPage): Rename to SenderPage.razor. 2026-10-02 14:30:11 +02:00
8378c33465 Refactor PDF rendering, UX, and security improvements
Refactored `PdfRenderController.cs` to replace JSON/base64 `byte[]` transport with binary streaming for PDF rendering, improving memory and latency performance for large files.

Introduced a DevExpress toast-based notification system using `DxToastProvider` and `IToastNotificationService` for consistent, actionable feedback.

Standardized request-to-domain mapping with AutoMapper/profile conventions or reflection-based mappers, enforced at the Core library level to reduce drift.

Addressed SQL injection risks by replacing dynamic SQL string composition with parameterized queries, conducted a repo-wide audit, and added CI guardrails.

Improved UX in `EnvelopeSenderPage.razor` by enforcing deterministic row double-click behavior and aligning row action buttons/tooltips.

Implemented SPA-style language switching for immediate UI culture updates without page reloads.

Updated the SQL Server connection string in `appsettings.json`.
2026-10-02 14:04:45 +02:00
c7c4e1194d Force full page reload on language change
Updated the `Navigation.NavigateTo` method in `LanguageControls.razor` to set the `forceLoad` parameter to `true`. This ensures that the application performs a full page reload when the language is changed, improving consistency in language updates.
2026-10-02 13:59:40 +02:00
3b367eff7f Remove EnvelopeSenderEditorPage.razor entirely
The `EnvelopeSenderEditorPage.razor` file was completely removed, including all Razor markup, Blazor components, JavaScript references, CSS styles, and C# code-behind logic.

Key changes:
- Removed the `/sender/editor_legacy` route and all associated imports (`@using`) and dependency injections (`@inject`).
- Deleted the entire HTML structure, including UI elements for PDF editing, signature placement, and receiver management.
- Removed all references to external CSS and JavaScript files.
- Deleted Blazor components like `DxPdfViewer`, `DxPopup`, and `DxTextBox`.
- Removed C# code for state management, lifecycle methods, event handlers, and helper methods.
- Eliminated localization, validation logic, session persistence, and cache management.
- Removed PDF manipulation logic (e.g., placeholder rendering with `PdfSharp`).
- Deleted receiver management logic, including email suggestions and validation.

This change removes the entire functionality of the `EnvelopeSenderEditorPage`, including its UI, backend logic, and external integrations.
2026-10-02 13:39:00 +02:00
37 changed files with 4298 additions and 1488 deletions

View File

@@ -63,6 +63,36 @@ This section explains every markdown file in the repo: what it contains, why it
---
## Markdown Audit Snapshot (2026-10-05)
The following table summarizes repository markdown files by practical status: completed, partially complete, or removable candidates.
| File | Type | Status | Missing / Gap | Removal Recommendation |
|---|---|---|---|---|
| `AGENTS.md` | Agent onboarding | Partially complete | Route/migration statements can drift from real code over time. | Keep |
| `COPILOT_CONTEXT.md` | Architecture reference | Partially complete | Some route/render details may be out of sync with other docs; needs regular reconciliation. | Keep |
| `FORM_APPLICATION_CONTEXT.md` | Legacy-to-modern feature spec | Partially complete | Large sections still marked not implemented / needs implementation. | Keep |
| `RECEIVER_PDF_VIEWER_CONTEXT.md` | Receiver PDF viewer deep-dive | Partially complete | Long migration plan needs periodic sync with active implementation. | Keep |
| `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` | Sender workflow implementation plan | Partially complete | No explicit implementation progress markers per phase. | Keep (until migration closes) |
| `SENDER_PDF_TOOLBAR_PLAN_EN.md` | Sender PDF toolbar plan | Incomplete | Explicit pending item remains (`Toolbar visual polish`). | Keep (until done) |
| `SENDER_EDITOR_WASM_PDF_EDITOR_IMPLEMENTATION_PLAN_en.md` | Sender editor WASM migration plan | Incomplete | Plan exists but completion/progress tracking is minimal. | Keep (until done) |
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md` | Deployment guide | Complete (operational) | No critical gap identified. | Keep |
| `envelope-generator-ui/README.md` | Angular scaffold README | Complete but generic | Template text only; low project-specific value. | Removable or replace with project-specific summary |
| `EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Third-party vendor README | Complete (vendor) | Not project workflow documentation. | Keep with vendor assets |
| `EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Third-party license | Complete (legal) | No gap. | Keep |
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/README.md` | Third-party vendor README | Complete (vendor) | Not project-specific guidance. | Keep with vendor assets |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/README.md` | Legacy PSPDFKit note | Outdated | Mentions deprecated PSPDFKit-based flow; may mislead if reused. | Removable/archive |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Duplicate vendor README | Redundant | Duplicate copy under `ignore/`. | Removable/archive |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/bootstrap-icons/README.md` | Vendor README in archive area | Secondary | Archive-only utility, not active docs. | Removable/archive |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Duplicate license in archive area | Redundant | Duplicate copy under `ignore/`. | Removable/archive (if legal policy allows) |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/README.md` | Duplicate Angular scaffold README | Redundant | Generic template + duplicate in archive path. | Removable/archive |
### Documentation Audit TODO
- [ ] Validate this markdown audit table against the current codebase and update statuses (`complete`, `partial`, `incomplete`, `removable`) when implementation changes land.
---
## Active Architecture (Post-Migration)
**Frontend:** Blazor Auto (Server+WASM hybrid)
@@ -250,6 +280,8 @@ Manual testing workflow:
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
- Implement SPA-style language switching so the UI culture updates immediately without a full page reload.
- Implement sender-scoped caching for "recent receiver suggestions" used during envelope creation (wizard/editor flows): cache key should include sender identity, support short TTL + invalidation hooks after successful envelope receiver writes, and prevent cross-sender suggestion leakage.
## Database

View File

@@ -0,0 +1,10 @@
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
public class RecentReceiverSuggestionDto
{
public required string FullName { get; init; }
public required string EmailAddress { get; init; }
public required DateTime LastUsedAt { get; init; }
}

View File

@@ -4,6 +4,7 @@
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>

View File

@@ -16,5 +16,9 @@ public class MappingProfile : Profile
public MappingProfile()
{
CreateMap<Receiver, ReceiverDto>();
CreateMap<EnvelopeReceiver, RecentReceiverSuggestionDto>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.Receiver.EmailAddress));
}
}

View File

@@ -0,0 +1,98 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
/// <summary>
/// Reads recent receiver suggestions for envelope creation.
/// </summary>
/// <remarks>
/// Sender scope is assigned via chain method to avoid exposing username mutation from API query binding.
/// </remarks>
public record ReadRecentReceiverSuggestionsQuery : IRequest<IReadOnlyList<RecentReceiverSuggestionDto>>
{
/// <summary>
/// Sender username used for scoping suggestion results.
/// </summary>
internal string? Username { get; private init; }
/// <summary>
/// Desired number of results.
/// </summary>
public int? Take { get; init; } = null;
/// <summary>
/// Returns a new query instance scoped to the specified sender username.
/// </summary>
/// <param name="username">Sender username from authenticated claims.</param>
/// <returns>Scoped query instance.</returns>
public ReadRecentReceiverSuggestionsQuery ForSender(string username)
{
return this with { Username = username };
}
}
/// <summary>
/// Handles <see cref="ReadRecentReceiverSuggestionsQuery"/>.
/// </summary>
public class ReadRecentReceiverSuggestionsQueryHandler(IRepository<EnvelopeReceiver> repository)
: IRequestHandler<ReadRecentReceiverSuggestionsQuery, IReadOnlyList<RecentReceiverSuggestionDto>>
{
/// <summary>
/// Retrieves distinct recent receiver suggestions grouped by receiver email.
/// </summary>
/// <param name="request">Suggestion query.</param>
/// <param name="cancel">Cancellation token.</param>
/// <returns>Ordered list of recent receiver suggestions.</returns>
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> Handle(ReadRecentReceiverSuggestionsQuery request, CancellationToken cancel)
{
var query = repository.Query
.AsNoTracking()
.Where(er => er.Receiver != null);
if (request.Username is not null)
query = query.Where(er => er.Envelope != null && er.Envelope.User.Username == request.Username);
// 1) For each email, compute the latest usage timestamp.
var latestPerEmail = query
.GroupBy(er => er.Receiver!.EmailAddress)
.Select(g => new
{
EmailAddress = g.Key,
LastUsedAt = g.Max(er => er.AddedWhen),
});
// 2) Join back to rows that match the latest timestamp per email.
var latestRows =
from er in query
join latest in latestPerEmail
on new { EmailAddress = er.Receiver!.EmailAddress, LastUsedAt = er.AddedWhen }
equals new { latest.EmailAddress, latest.LastUsedAt }
select new
{
EmailAddress = er.Receiver!.EmailAddress,
FullName = er.Name,
LastUsedAt = er.AddedWhen,
};
// 3) Tie-break: if multiple rows share the same latest timestamp for an email,
// pick a deterministic name via Max(FullName), then order by most recent usage.
var suggestions = latestRows
.GroupBy(x => new { x.EmailAddress, x.LastUsedAt })
.Select(g => new RecentReceiverSuggestionDto
{
EmailAddress = g.Key.EmailAddress,
FullName = g.Max(x => x.FullName) ?? string.Empty,
LastUsedAt = g.Key.LastUsedAt,
})
.OrderByDescending(x => x.LastUsedAt)
.ThenBy(x => x.EmailAddress);
return request.Take is int take
? await suggestions.Take(take).ToListAsync(cancel)
: await suggestions.ToListAsync(cancel);
}
}

View File

@@ -123,6 +123,9 @@
<data name="Back" xml:space="preserve">
<value>Zurück</value>
</data>
<data name="Next" xml:space="preserve">
<value>Weiter</value>
</data>
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
<value>Klicken Sie auf den</value>
</data>
@@ -564,6 +567,12 @@
<data name="AddReceiver" xml:space="preserve">
<value>Empfänger hinzufügen</value>
</data>
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
<value>Zuletzt verwendete Empfänger</value>
</data>
<data name="Skip" xml:space="preserve">
<value>Überspringen</value>
</data>
<data name="DeleteReceiver" xml:space="preserve">
<value>Empfänger löschen</value>
</data>

View File

@@ -123,6 +123,9 @@
<data name="Back" xml:space="preserve">
<value>Back</value>
</data>
<data name="Next" xml:space="preserve">
<value>Next</value>
</data>
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
<value>Click the</value>
</data>
@@ -564,6 +567,12 @@
<data name="AddReceiver" xml:space="preserve">
<value>Add Receiver</value>
</data>
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
<value>Recently used recipients</value>
</data>
<data name="Skip" xml:space="preserve">
<value>Skip</value>
</data>
<data name="DeleteReceiver" xml:space="preserve">
<value>Delete Receiver</value>
</data>

View File

@@ -123,6 +123,9 @@
<data name="Back" xml:space="preserve">
<value>Retour</value>
</data>
<data name="Next" xml:space="preserve">
<value>Suivant</value>
</data>
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
<value>Cliquez sur</value>
</data>
@@ -564,6 +567,12 @@
<data name="AddReceiver" xml:space="preserve">
<value>Ajouter un destinataire</value>
</data>
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
<value>Destinataires recents</value>
</data>
<data name="Skip" xml:space="preserve">
<value>Passer</value>
</data>
<data name="DeleteReceiver" xml:space="preserve">
<value>Supprimer le destinataire</value>
</data>

View File

@@ -276,4 +276,61 @@
<data name="YesWithAttachment" xml:space="preserve">
<value>Yes, with Attachment</value>
</data>
</root>
<data name="ElectronicSignature" xml:space="preserve">
<value>Electronic Signature</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Envelope Type</value>
</data>
<data name="Language" xml:space="preserve">
<value>Language</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Certification</value>
</data>
<data name="TFAEnabled" xml:space="preserve">
<value>2FA enabled</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Use Access Code</value>
</data>
<data name="FinalEmailToCreator" xml:space="preserve">
<value>Final Email to Creator</value>
</data>
<data name="FinalEmailToReceivers" xml:space="preserve">
<value>Final Email to Receivers</value>
</data>
<data name="SendReminderEmails" xml:space="preserve">
<value>Send Reminder Emails</value>
</data>
<data name="FirstReminderDays" xml:space="preserve">
<value>First Reminder (Days)</value>
</data>
<data name="ReminderIntervalDays" xml:space="preserve">
<value>Reminder Interval (Days)</value>
</data>
<data name="ExpiresWhenDays" xml:space="preserve">
<value>Expires After (Days)</value>
</data>
<data name="ExpiresWarningWhenDays" xml:space="preserve">
<value>Expiry Warning (Days)</value>
</data>
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
<value>Envelope Settings</value>
</data>
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
<value>Advanced</value>
</data>
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
<value>Select Template</value>
</data>
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
<value>Please enter a reminder interval greater than 0 days.</value>
</data>
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
<value>Please enter valid days for the first reminder.</value>
</data>
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
<value>The expiry warning cannot be after the expiry date.</value>
</data>
</root>

View File

@@ -113,4 +113,61 @@
<data name="MessageInvitationSent" xml:space="preserve">
<value>Invitation envoyee</value>
</data>
<data name="ElectronicSignature" xml:space="preserve">
<value>Signature electronique</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Type d'enveloppe</value>
</data>
<data name="Language" xml:space="preserve">
<value>Langue</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Certification</value>
</data>
<data name="TFAEnabled" xml:space="preserve">
<value>2FA active</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Utiliser un code d'acces</value>
</data>
<data name="FinalEmailToCreator" xml:space="preserve">
<value>E-mail final au createur</value>
</data>
<data name="FinalEmailToReceivers" xml:space="preserve">
<value>E-mail final aux destinataires</value>
</data>
<data name="SendReminderEmails" xml:space="preserve">
<value>Envoyer des e-mails de rappel</value>
</data>
<data name="FirstReminderDays" xml:space="preserve">
<value>Premier rappel (jours)</value>
</data>
<data name="ReminderIntervalDays" xml:space="preserve">
<value>Intervalle de rappel (jours)</value>
</data>
<data name="ExpiresWhenDays" xml:space="preserve">
<value>Expire apres (jours)</value>
</data>
<data name="ExpiresWarningWhenDays" xml:space="preserve">
<value>Avertissement d'expiration (jours)</value>
</data>
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
<value>Parametres de l'enveloppe</value>
</data>
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
<value>Avance</value>
</data>
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
<value>Selectionner un modele</value>
</data>
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
<value>Veuillez saisir un intervalle de rappel superieur a 0 jour.</value>
</data>
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
<value>Veuillez saisir des jours valides pour le premier rappel.</value>
</data>
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
<value>L'avertissement d'expiration ne peut pas etre apres la date d'expiration.</value>
</data>
</root>

View File

@@ -279,4 +279,61 @@
<data name="YesWithAttachment" xml:space="preserve">
<value>Ja, mit Anhang</value>
</data>
</root>
<data name="ElectronicSignature" xml:space="preserve">
<value>Elektronische Signatur</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Umschlagtyp</value>
</data>
<data name="Language" xml:space="preserve">
<value>Sprache</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Zertifizierung</value>
</data>
<data name="TFAEnabled" xml:space="preserve">
<value>2FA aktiviert</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Zugriffscode verwenden</value>
</data>
<data name="FinalEmailToCreator" xml:space="preserve">
<value>Finale E-Mail an Ersteller</value>
</data>
<data name="FinalEmailToReceivers" xml:space="preserve">
<value>Finale E-Mail an Empfänger</value>
</data>
<data name="SendReminderEmails" xml:space="preserve">
<value>Erinnerungs-E-Mails senden</value>
</data>
<data name="FirstReminderDays" xml:space="preserve">
<value>Erste Erinnerung (Tage)</value>
</data>
<data name="ReminderIntervalDays" xml:space="preserve">
<value>Erinnerungsintervall (Tage)</value>
</data>
<data name="ExpiresWhenDays" xml:space="preserve">
<value>Läuft ab nach (Tage)</value>
</data>
<data name="ExpiresWarningWhenDays" xml:space="preserve">
<value>Ablaufwarnung (Tage)</value>
</data>
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
<value>Envelope-Einstellungen</value>
</data>
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
<value>Erweitert</value>
</data>
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
<value>Vorlage auswählen</value>
</data>
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
<value>Bitte geben Sie ein Erinnerungsintervall größer als 0 Tage ein.</value>
</data>
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
<value>Bitte geben Sie gültige Tage für die erste Erinnerung ein.</value>
</data>
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
<value>Die Ablaufwarnung darf nicht nach dem Ablaufdatum liegen.</value>
</data>
</root>

View File

@@ -105,10 +105,10 @@ Public Class frmEnvelopeMainData
End If
End Sub
Private Function TranslateCertificationType(pType As CertificationType) As String
Return Resources.Model.ResourceManager.GetString(pType.ToString())
Return CommonServices.My.Resources.Model.ResourceManager.GetString(pType.ToString())
End Function
Private Function TranslateFinalEmailType(pType As FinalEmailType) As String
Return Resources.Model.ResourceManager.GetString(pType.ToString())
Return CommonServices.My.Resources.Model.ResourceManager.GetString(pType.ToString())
End Function
Private Sub btOK_Click(sender As Object, e As EventArgs) Handles btOK.Click

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.6" />
<PackageReference Include="Pkcs11Interop" Version="5.3.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,389 @@
# EnvelopeGenerator.Infrastructure.Crypt
PKCS#11-based cryptographic infrastructure for FES (Advanced Electronic Signature) workflows.
Official references:
- SoftHSMv2 (official): `https://github.com/softhsm/SoftHSMv2`
- OpenSC `pkcs11-tool` docs: `https://github.com/OpenSC/OpenSC/wiki/Using-pkcs11-tool-and-OpenSSL`
- PKCS#11 standard overview (OASIS): `https://www.oasis-open.org/committees/pkcs11/`
---
## 1) Scope
This project is the crypto layer only.
- Signs hashes via keys in HSM/SoftHSM.
- Reads certificates from HSM/SoftHSM.
- Verifies signatures.
- Exposes abstractions for Application/Server integration.
This project is not:
- a REST API,
- a UI module,
- a full document orchestration workflow.
---
## 2) SoftHSM and PKCS#11 basics
- SoftHSM is a software implementation of an HSM.
- It does not expose REST endpoints.
- Access is via PKCS#11 shared library (`.so`/`.dll`).
- App loads module path and performs PKCS#11 operations.
Supported deployment patterns:
1. Direct module (`libsofthsm2.so`, `softhsm2-x64.dll`, etc.)
2. PKCS#11 proxy module (remote HSM/SoftHSM backend)
Critical platform rule:
- A Windows process cannot load a Linux `.so` directly.
- `Crypt:Pkcs11:LibraryPath` must always point to a native library compatible with the host OS of `EnvelopeGenerator.Server`.
---
## 2.1) Windows Server + Linux SoftHSM architecture (decision TODO)
Current business requirement: `EnvelopeGenerator.Server` runs on Windows, SoftHSM runs on Linux.
This repository must use one of the following three paths:
1. Linux signing service (recommended)
- Windows `EnvelopeGenerator.Server` calls HTTPS signing API.
- Linux service performs PKCS#11 operations against SoftHSM.
- Private keys stay on Linux.
2. PKCS#11 proxy bridge
- Windows app loads a Windows PKCS#11 proxy client DLL.
- Proxy forwards operations to Linux-side PKCS#11 endpoint connected to SoftHSM.
3. Local Windows SoftHSM (dev/test only, optional)
- SoftHSM is also installed on Windows.
- App talks to local Windows PKCS#11 module.
TODO - architecture lock-in:
- [ ] Select one path as production standard (`1` is recommended).
- [ ] Document final runtime topology (hosts, ports, TLS, auth).
- [ ] Define where audit metadata (IP/email/phone/device) is stored and signed/linked.
- [ ] Update `SignatureController.Submit` integration contract for FES branch.
- [ ] Add environment-specific runbook for Windows host + Linux HSM operations.
---
## 3) FES flow mapping
- Step 6: hash is sent to HSM signing operation -> `ISignatureProvider`
- Step 7: signature and certificate context are resolved -> `Pkcs11SignatureProvider`, `ICertificateProvider`
- Step 8: PDF embedding + audit trail are completed by upper layers; this project provides crypto primitives
---
## 4) Service surface
Main interfaces:
- `ISignatureProvider`
- `ICertificateProvider`
- `ISignatureVerifier`
- `ICryptographicHealthCheck`
DI entry point:
- `CryptDependencyInjection.AddCryptInfrastructure(...)`
---
## 5) Runtime configuration
Configuration section: `Crypt:Pkcs11`
```json
{
"Crypt": {
"Pkcs11": {
"LibraryPath": "/usr/lib/softhsm/libsofthsm2.so",
"SlotId": 1720207650,
"TokenLabel": "DevToken",
"UserPin": "***",
"PrivateKeyLabel": "sign-key",
"PrivateKeyIdHex": null,
"CertificateLabel": "sign-cert",
"CertificateIdHex": null
}
}
}
```
Security rules:
- Never commit real PINs.
- Use environment variables or secret vault.
- Use test-only tokens for development.
Production naming convention used in this repository:
- Token label: `CompanyProdToken`
- Private key label: `company-sign-key-v1`
- Certificate label: `company-sign-cert-v1`
These names are also exposed as constants in `EnvelopeGenerator.Infrastructure.Crypt/Options/Pkcs11ProviderOptions.cs`.
---
## 6) Test configuration source
Crypt tests read PKCS#11 values from `EnvelopeGenerator.Tests/appsettings.json` under `Crypt:Pkcs11`.
Required keys:
- `Crypt:Pkcs11:LibraryPath`
- `Crypt:Pkcs11:SlotId` (or `Crypt:Pkcs11:TokenLabel`)
- `Crypt:Pkcs11:UserPin`
- `Crypt:Pkcs11:PrivateKeyLabel`
- `Crypt:Pkcs11:CertificateLabel`
No environment variable is required for crypt tests in this repository anymore.
---
## 7) Full SoftHSM lifecycle runbook (project scope)
### 7.1 Discover module path
```bash
ldconfig -p | grep softhsm
find /usr -name "libsofthsm2.so" 2>/dev/null
```
### 7.2 List slots/tokens
```bash
softhsm2-util --show-slots
```
### 7.3 Create new token (recommended for new company/test tenant)
```bash
softhsm2-util --init-token --slot <uninitialized-slot> --label <token-label> --so-pin <so-pin> --pin <user-pin>
```
Important:
- After init, token is reassigned to a new slot id.
- Re-run `--show-slots` and use the new slot id.
### 7.4 Login and inspect objects
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <user-pin> --list-objects
```
### 7.5 Generate company signing key pair
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <user-pin> \
--keypairgen --key-type rsa:3072 --label sign-key --id 01
```
### 7.6 Import company certificate
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <user-pin> \
--write-object company-sign-cert.der --type cert --label sign-cert --id 01
```
### 7.7 Read object metadata (for diagnostics)
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <user-pin> --list-objects
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <user-pin> --list-mechanisms
```
### 7.8 Update operations
PKCS#11 objects are usually immutable for key/cert payload; updates are done by rotation:
1. Create/import new key/cert with new label or id.
2. Update app config/env labels.
3. Validate with integration tests.
4. Delete old objects when safe.
PIN operations:
- User PIN change (while logged in):
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <old-user-pin> --change-pin --new-pin <new-user-pin>
```
- User PIN reset via SO credentials:
```bash
softhsm2-util --init-pin --slot <slot-id> --so-pin <so-pin> --pin <new-user-pin>
```
### 7.9 Delete operations
Delete by label/id/type:
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <slot-id> --login --pin <user-pin> \
--delete-object --type cert --label sign-cert
```
Token-level destructive reset (do not use unless approved):
```bash
softhsm2-util --delete-token --token <token-label>
```
---
## 8) New company onboarding checklist
For company-level signing model (one company key/cert pair):
1. Create token with company label (or assign dedicated token in remote HSM).
2. Generate key pair in token.
3. Obtain CA certificate and import into token.
4. Record secure runtime values:
- module path
- slot id or token label
- user pin
- private key label
- certificate label
5. Run crypt integration tests.
6. Store values in vault and wire app environment.
---
## 9) Rotation/update checklist
1. Create new key/cert (`sign-key-v2`, `sign-cert-v2`).
2. Configure app env to new labels.
3. Run integration tests.
4. Deploy.
5. Keep old key/cert during grace period.
6. Remove old key/cert after legal/operational confirmation.
---
## 10) Running tests
Before running, configure `EnvelopeGenerator.Tests/appsettings.json` -> `Crypt:Pkcs11` with real Linux SoftHSM values.
Run crypt tests:
```bash
dotnet test "EnvelopeGenerator.Tests/EnvelopeGenerator.Tests.csproj" -f net8.0 --filter "FullyQualifiedName~EnvelopeGenerator.Tests.Application.Crypt"
```
---
## 11) Common failures
- `CKR_PIN_INCORRECT`: wrong user pin for that token.
- `CRYPT_PROVIDER_UNAVAILABLE`: bad module path or native load issue.
- `CRYPT_SLOT_NOT_FOUND`: wrong slot id or token label.
- `CRYPT_LOGIN_FAILED`: login failed (pin/token mismatch).
- `CRYPT_KEY_NOT_FOUND`: key label/id mismatch.
- `CRYPT_CERT_NOT_FOUND`: certificate label/id mismatch.
---
## 12) Integration note
Current implementation uses `CKM_SHA256_RSA_PKCS` while caller provides hash bytes. Validate mechanism semantics in your provider/HSM policy. If provider expects raw payload instead of digest for selected mechanism, signing flow must be adjusted.
---
## 13) Production-first onboarding (single-line checkpoints)
This section is the authoritative path for production-style setup (critical documents, FES).
### 13.1 Step 1 - Initialize dedicated company token
One-line command:
```bash
mkdir -p ./fes-prod && cd ./fes-prod && export SO_PIN="$(openssl rand -hex 16)" USER_PIN="$(openssl rand -hex 12)" && softhsm2-util --init-token --slot <uninitialized-slot> --label CompanyProdToken --so-pin "$SO_PIN" --pin "$USER_PIN" && softhsm2-util --show-slots
```
Checkpoint:
- Token appears as initialized with label `CompanyProdToken`.
- SoftHSM reassigns token to a new slot ID; use that slot ID in all next commands.
Security note:
- Do not print/store PINs in shell history in real production operations.
- Persist secrets in a vault or protected secret store.
### 13.2 Step 2 - Generate company signing key pair inside HSM
One-line command:
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <company-slot-id> --login --pin "$USER_PIN" --keypairgen --key-type rsa:3072 --label company-sign-key-v1 --id 01 && pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <company-slot-id> --login --pin "$USER_PIN" --list-objects
```
Checkpoint:
- `Private Key Object` and `Public Key Object` exist for label `company-sign-key-v1`, id `01`.
- Private key remains non-extractable in token.
### 13.3 Step 3 - Obtain CA-issued certificate for HSM key
Required policy:
- Certificate must be issued by enterprise CA for the key generated in token.
- Do not use self-signed cert for production signing identity.
Operational note:
- CSR generation method depends on installed PKCS#11 OpenSSL integration (`engine_pkcs11` / provider).
- After CA issues certificate, import certificate DER into token with matching id/label family.
OpenSSL 3 prerequisite check:
```bash
openssl version && openssl engine -t -c 2>/dev/null | grep -i pkcs11 || true
```
If no PKCS#11 engine/provider is listed, install integration packages first (distribution specific), then continue.
### 13.4 Step 4 - Import CA certificate into token
One-line command:
```bash
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <company-slot-id> --login --pin "$USER_PIN" --write-object ./company-sign-cert-v1.der --type cert --id 01 --label company-sign-cert-v1 && pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot <company-slot-id> --login --pin "$USER_PIN" --list-objects
```
Checkpoint:
- `Certificate Object` exists with id `01` and expected label.
### 13.5 Step 5 - Wire runtime/test configuration
Server runtime (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`) must include:
- `Crypt:Pkcs11:LibraryPath` = native PKCS#11 library path for the server host OS
- Windows host: Windows DLL path (vendor DLL or proxy client DLL)
- Linux host: Linux `.so` path
- `Crypt:Pkcs11:SlotId` = `2021549410` (or current company slot)
- `Crypt:Pkcs11:TokenLabel` = `CompanyProdToken`
- `Crypt:Pkcs11:UserPin` = real user pin
- `Crypt:Pkcs11:PrivateKeyLabel` = `company-sign-key-v1`
- `Crypt:Pkcs11:CertificateLabel` = `company-sign-cert-v1`
Test runtime (`EnvelopeGenerator.Tests/appsettings.json`) must mirror the same `Crypt:Pkcs11` values.
Then run tests:
```bash
dotnet test "EnvelopeGenerator.Tests/EnvelopeGenerator.Tests.csproj" -f net8.0 --filter "FullyQualifiedName~EnvelopeGenerator.Tests.Application.Crypt"
```

View File

@@ -0,0 +1,301 @@
# FES Signature Infrastructure Plan
## 1. Goal
Build a reusable and modular cryptography infrastructure in `EnvelopeGenerator.Infrastructure.Crypt` to support FES (Advanced Electronic Signature) workflows.
This module must:
- use PKCS#11 providers (SoftHSMv2 first, real HSM later),
- stay independent from UI/API concerns,
- expose clean interfaces to `Application` layer use-cases,
- support future extensions (timestamping, certificate chain validation, multiple key algorithms).
---
## 2. Context and Constraints
1. SoftHSMv2 is **not** a REST service. It is a PKCS#11 module loaded as a native library.
2. `EnvelopeGenerator.Server` is presentation only; cryptographic implementation belongs to infrastructure.
3. Clean Architecture boundaries must be respected:
- `Application` depends on abstractions.
- `Infrastructure.Crypt` implements abstractions.
4. Initial target framework is `net8.0`.
5. We need production-ready behavior, not only PoC signing.
---
## 3. High-Level Design
## 3.1 Architecture slice
- **Application layer (contracts/use-cases):**
- request signing hash/document,
- request certificate/public key material,
- request verification,
- consume signature evidence result.
- **Infrastructure.Crypt layer (this project):**
- PKCS#11 session handling,
- key/certificate object discovery,
- hash signing operations,
- local cryptographic verification,
- certificate parsing helpers,
- error normalization.
## 3.2 Provider model
Define provider-agnostic APIs and add one concrete provider first:
- `Pkcs11SignatureProvider` (primary)
- future providers:
- `RemoteSigningProvider` (if a dedicated signing gateway is introduced)
- `MockSignatureProvider` (for deterministic test scenarios)
---
## 4. Deliverables (Phase by Phase)
## Phase 0 - Contracts and Domain Language
Create neutral models and interfaces in `Infrastructure.Crypt` (or shared abstractions if needed later):
- `SigningAlgorithm` (enum/value object)
- `Pkcs11ProviderOptions`
- `KeyLocator` (label/id/slot/token selectors)
- `SignHashRequest`
- `SignHashResult`
- `VerifySignatureRequest`
- `VerifySignatureResult`
- `CertificateDescriptor`
- `SignatureEvidence` (technical evidence payload)
Interfaces:
- `ISignatureProvider`
- `ICertificateProvider`
- `ISignatureVerifier`
- `ICryptographicHealthCheck`
Acceptance criteria:
- no dependency on ASP.NET types,
- no UI/API naming leakage,
- all model names are business-neutral.
## Phase 1 - PKCS#11 Core Adapter (SoftHSM-ready)
Implement:
- `Pkcs11LibraryLoader`
- `Pkcs11SessionFactory`
- `Pkcs11ObjectFinder`
- `Pkcs11SignatureProvider`
- `Pkcs11CertificateProvider`
Capabilities:
1. load module path from options (`libsofthsm2.so` or proxy module when required),
2. enumerate slots/tokens,
3. open read-write session,
4. login with user pin,
5. find private key by label/id,
6. sign digest with selected mechanism,
7. read certificate object by label/id.
Acceptance criteria:
- deterministic disposal of sessions,
- no leaked native handles,
- detailed typed errors (not raw exception strings only).
## Phase 2 - Verification and Certificate Utilities
Implement:
- `DefaultSignatureVerifier`
- `X509CertificateParser`
- optional `CertificateChainValidator` (toggleable)
Capabilities:
1. verify signature using returned certificate/public key,
2. expose certificate metadata (`thumbprint`, `subject`, `issuer`, `notBefore`, `notAfter`),
3. map validation failures into stable error codes.
Acceptance criteria:
- same request yields same verification outcome,
- invalid signature returns structured negative result.
## Phase 3 - Evidence Builder
Implement:
- `SignatureEvidenceBuilder`
Evidence payload should contain:
- transaction/correlation id,
- algorithm,
- hash metadata,
- key locator snapshot (safe subset),
- certificate fingerprint and subject,
- sign timestamp (UTC),
- verification outcome,
- provider type (`PKCS11`).
Acceptance criteria:
- no secret values in evidence (pin, private key id internals),
- evidence serializable for storage/audit.
## Phase 4 - Dependency Injection Module
Implement:
- `CryptInfrastructureDependencyInjection` extension methods
Methods:
- `AddCryptInfrastructure(...)`
- `AddPkcs11Provider(...)`
Acceptance criteria:
- one-line registration from composition root,
- options validation on startup.
## Phase 5 - Test Suite (`EnvelopeGenerator.Tests`)
Add test categories for `net8.0`:
1. `Unit`:
- request validation,
- algorithm mapping,
- evidence building,
- error mapping.
2. `Integration` (conditional/manual profile):
- SoftHSM slot listing,
- login,
- sign hash,
- verify,
- certificate read.
Acceptance criteria:
- unit tests run in CI without native SoftHSM,
- integration tests run when module path + pins are provided via environment variables.
---
## 5. Proposed Project Structure
```text
EnvelopeGenerator.Infrastructure.Crypt/
Abstractions/
ISignatureProvider.cs
ICertificateProvider.cs
ISignatureVerifier.cs
ICryptographicHealthCheck.cs
Models/
SigningAlgorithm.cs
SignHashRequest.cs
SignHashResult.cs
VerifySignatureRequest.cs
VerifySignatureResult.cs
SignatureEvidence.cs
CertificateDescriptor.cs
KeyLocator.cs
Options/
Pkcs11ProviderOptions.cs
Pkcs11/
Pkcs11LibraryLoader.cs
Pkcs11SessionFactory.cs
Pkcs11ObjectFinder.cs
Pkcs11SignatureProvider.cs
Pkcs11CertificateProvider.cs
Verification/
DefaultSignatureVerifier.cs
X509CertificateParser.cs
Evidence/
SignatureEvidenceBuilder.cs
DependencyInjection/
CryptInfrastructureDependencyInjection.cs
Exceptions/
CryptographicOperationException.cs
Pkcs11ProviderException.cs
```
---
## 6. Configuration Strategy
Use options (bound from host config) with environment override support:
- `Crypt:Provider = PKCS11`
- `Crypt:Pkcs11:LibraryPath`
- `Crypt:Pkcs11:SlotId` or token selectors
- `Crypt:Pkcs11:UserPin` (from secret store/env, not plain committed value)
- `Crypt:Pkcs11:PrivateKeyLabel`
- `Crypt:Pkcs11:CertificateLabel`
- `Crypt:Pkcs11:LoginType`
Validation rules:
- library path required,
- at least one key locator required,
- pin required for signing operations.
---
## 7. Error Model
Define stable error codes to prevent provider-specific leakage:
- `CRYPT_PROVIDER_UNAVAILABLE`
- `CRYPT_SLOT_NOT_FOUND`
- `CRYPT_LOGIN_FAILED`
- `CRYPT_KEY_NOT_FOUND`
- `CRYPT_CERT_NOT_FOUND`
- `CRYPT_SIGN_FAILED`
- `CRYPT_VERIFY_FAILED`
Map low-level exceptions to these codes with safe diagnostics.
---
## 8. Non-Functional Requirements
1. **Security:** never log pins or raw private key handles.
2. **Reliability:** deterministic cleanup (`IDisposable`/`IAsyncDisposable`).
3. **Observability:** structured logs with correlation id.
4. **Performance:** avoid repeated login per operation when a safe session strategy is available.
5. **Extensibility:** algorithm/provider mapping must be open for extension.
---
## 9. Implementation Order (Small Tasks)
1. Add models + interfaces.
2. Add options + validators.
3. Add PKCS#11 low-level loader/session wrappers.
4. Implement `Pkcs11SignatureProvider` for hash signing.
5. Implement certificate retrieval.
6. Implement verifier.
7. Implement evidence builder.
8. Add DI registration extension.
9. Add unit tests.
10. Add optional integration tests (SoftHSM profile).
---
## 10. Definition of Done
Done means:
1. `Infrastructure.Crypt` exposes generic cryptographic services usable by Application use-cases.
2. SoftHSM-backed PKCS#11 sign/verify path works in integration tests.
3. All sensitive configuration is externalized.
4. Structured error codes are returned for expected failure scenarios.
5. Documentation exists for host registration and required settings.

View File

@@ -0,0 +1,228 @@
# FES (Advanced Electronic Signature) Summary and Integration Plan - EN
This document summarizes the shared diagram flow (`Ablauf SignFlow Unternehmenszertifikat`) and provides a small-step integration plan for the `EnvelopeGenerator` solution.
---
## 1) Simplified flow summary
Flow logic:
1. The sender uploads a document and defines signers.
2. If a valid company certificate already exists in HSM, it is used with the company key pair.
3. If no valid company certificate exists, CA issuance/renewal flow is required.
4. The signer receives an e-mail with a signing link.
5. The signer completes SMS OTP (2FA) and performs UI signature interaction.
6. Backend computes the hash for the current signing stage and sends it to HSM.
7. HSM signs the hash with the private key and returns signature + certificate context.
8. Backend embeds digital signature and certificate into PDF and writes audit trail.
9. After all signers complete, the final artifact is archived.
10. The final signed document is distributed to signers.
Important: The visible UI signature is not yet the legal electronic signature. The legal signature is produced in backend+HSM steps.
---
## 2) Relation to current codebase (as-is)
- `EnvelopeGenerator.Server` is the presentation layer host (Blazor + API hosting).
- Business flow is handled in `Application` via MediatR pipelines.
- Current receiver flow is centered around UI overlay/signature capture.
- Missing FES core: PKCS#11 hash-sign-verify + certificate-based PDF embedding pipeline.
Critical deployment fact:
- `EnvelopeGenerator.Server` is planned to run on Windows.
- SoftHSM is currently planned to run on Linux.
- A Windows process cannot directly load Linux PKCS#11 module (`libsofthsm2.so`).
- Therefore, architecture must include a compatibility bridge (service or proxy) unless HSM/SoftHSM is local to Windows.
Conclusion: FES is not a replacement of the current flow; it is a cryptographic hardening layer behind it.
---
## 3) Why SoftHSM tests are foundational
The tests defined in `SOFTHSM_TEST_KILAVUZU_TR.md` are the technical prerequisite for FES:
- `slots/login` -> HSM connectivity and authentication
- `certificate` -> certificate retrieval path
- `sign` -> hash signing with private key
- `verify` -> signature and certificate consistency validation
Without these tests passing, diagram steps 6-8 should not go live.
---
## 4) Target architecture (clean architecture aligned)
### Deployment topology options (must choose one)
1. **Linux signing service (recommended)**
- Windows `EnvelopeGenerator.Server` calls Linux signing API over HTTPS.
- Linux signing service performs PKCS#11 operations against SoftHSM.
- Private keys never leave Linux/HSM boundary.
2. **PKCS#11 proxy bridge**
- Windows host loads a Windows PKCS#11 proxy client DLL.
- Proxy forwards PKCS#11 operations to Linux side connected to SoftHSM.
3. **Windows-local SoftHSM (dev/test)**
- SoftHSM also installed on Windows and used locally.
- Useful for local testing, not preferred production topology.
Decision note:
- For production-critical FES, choose option 1 by default unless enterprise HSM policy requires option 2.
- Keep option 3 for development convenience only.
### Layer responsibilities
- `EnvelopeGenerator.Server`:
- API endpoints, auth orchestration, input validation.
- FES branch routing: standard-sign path vs external signing-service/proxy path.
- No private-key material handling.
- `EnvelopeGenerator.Application`:
- Use cases such as `SignWithHsmCommand`.
- MediatR pipeline orchestration for status/history/audit.
- FES evidence model aggregation (ip/email/phone/user-agent/otp proof/timestamps).
- `EnvelopeGenerator.Infrastructure`:
- Implementations for `IPkcs11Service`, `IPdfSignatureService`, `ITimestampService`.
- Pkcs11Interop integration and cryptographic operations.
- Optional client for Linux signing service if topology option 1 is selected.
- `Domain`:
- Signature evidence and audit-trail models.
---
## 5) Small-step integration plan
## Phase 0 - Scope and contracts
1. Finalize signing algorithms (for example `SHA256_RSA_PKCS`).
2. Confirm strategy: company certificate vs signer-specific certificate.
3. Freeze mandatory audit fields:
- identity, OTP proof, IP, user-agent, transaction id, timestamps, hash metadata.
4. Freeze deployment topology decision:
- option 1 (Linux signing service), option 2 (PKCS#11 proxy), or option 3 (Windows-local SoftHSM).
5. Define production host mapping:
- where `EnvelopeGenerator.Server` runs,
- where HSM/SoftHSM runs,
- where certificate lifecycle is managed.
Deliverable: Technical decision record (ADR-style markdown).
## Phase 1 - PKCS#11 connector (infrastructure)
1. Define `IPkcs11Service` interface.
2. Add `Pkcs11Interop` implementation.
3. Add config model:
- library path, slot, token label, key label, certificate label.
4. Ensure secure secret handling for PIN.
5. Ensure host-compatible module loading:
- Windows server must load Windows DLL,
- Linux server must load Linux `.so`.
Deliverable: Testable PKCS#11 service with health/slot/login/sign/verify capabilities.
## Phase 1.5 - Cross-host bridge implementation
1. If option 1 selected:
- implement Linux signing service API contract (`/sign`, `/certificate`, `/health`),
- implement authenticated HTTP client in `EnvelopeGenerator.Server`.
2. If option 2 selected:
- install/configure PKCS#11 proxy client DLL on Windows,
- install/configure proxy server on Linux and bind to SoftHSM.
Deliverable: Windows-to-Linux signing communication path proven end-to-end.
## Phase 2 - Application use case
1. Add `SignDocumentHashCommand` (or equivalent).
2. Pipeline flow:
- compute/receive hash,
- sign with HSM,
- attach certificate context,
- verify,
- produce evidence object.
3. Add resilient error mapping and retry strategy.
Deliverable: HSM signing use case in Application layer.
## Phase 3 - PDF signature embedding
1. Implement PDF signature embedding via `IPdfSignatureService`.
2. Build signer-specific audit-trail pages.
3. Append all audit pages to final PDF artifact.
Deliverable: Signed PDF with certificate context and audit trail pages.
## Phase 4 - Presentation/API integration
1. Integrate FES option into submit flow (feature flag guarded).
2. Add operational endpoints:
- HSM health/check
- signature evidence query (restricted roles)
3. Keep DTOs safe; avoid exposing sensitive cryptographic material.
4. In `SignatureController.Submit`, add FES branch that sends signing payload + audit context to selected signing backend.
Deliverable: API-level orchestration with minimal UI disruption.
## Phase 5 - Audit, archive, distribution
1. Persist signature evidence records.
2. Include hash/cert metadata in archival package.
3. Add signed artifact reference to distribution e-mails.
Deliverable: Alignment with diagram steps 9-10.
## Phase 6 - Testing and operations
1. Postman collection + Newman automation.
2. Positive/negative matrix:
- wrong pin, wrong slot, missing cert, expired cert, HSM timeout.
3. Observability:
- structured logs, correlation id, latency metrics.
Deliverable: Production readiness checklist.
---
## 6) Transition strategy from test API to production flow
1. Keep `/api/pkcs11-test/*` only for non-production.
2. In production, use the same internal services through business endpoints only.
3. Roll out by feature flag:
- `FesEnabled=false` initially
- pilot sender group
- full rollout
4. Keep explicit environment matrix:
- local dev (option 3),
- integration/staging (option 1 or 2),
- production (option 1 or 2 only).
---
## 7) Risks and mitigations
- Certificate expiry risk -> proactive monitoring and renewal alerting.
- HSM connectivity risk -> retry policy, circuit breaker, fallback queue.
- PIN/secret exposure risk -> vault-backed secrets and rotation policy.
- Verification failure risk -> block PDF sealing and move envelope to technical hold state.
- Cross-host connectivity risk -> mTLS/JWT auth + timeout/retry/circuit breaker + health probe.
---
## 8) Short done checklist
- [ ] PKCS#11 service interface + implementation
- [ ] Application command + pipeline behaviors
- [ ] PDF signature embedding
- [ ] Audit-trail generation
- [ ] API integration + feature flag
- [ ] Postman/Newman regression suite
- [ ] Operational runbook and alerting rules

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,84 @@
10/6/26, 10:46 PM
Test SoftHSM : SWINFRA-54
Projects / Infrastruktur Software / Issues /
Enter search…
SWINFRA-54
All…
Created by Marvin Kamm 3 months ago
Visible to issue readers 1
Updated by Matthias Dewald about 1 month ago
# **Test SoftHSM**
Project Priorität Status **SWI** Infrastruktur Softwa H Hoch O Offen re… Bearbeiter Fälligkeit Projekteinfluss MD _ ? ? Matthias Dewald Kein fälligkeit Kein projektbezug Boards No visible boards
Bitte eine Test-VM mit der Software SoftHSM bereitstellen.
Laut Info von @Henning Emrich soll die Installation unter Debian Linux deutlich einfacher sein, als unter Windows. Bitte prüfen.
@Hakan Tek & @OlgunR
Sollen bitte die API Ansteuerung testen
FYI
@Marlon Schreiber @Jan-Ulrich Hoss @Henning Emrich @Hakan Tek @OlgunR
**Attachments** 1
Files Attached to Comments
**PDF**
Ablauf SignFlow Un ternehme… 83 kB
MD _ **Matthias Dewald** Commented 3 months ago Ist für Dienstag 30.6 10:00 Uhr Eingeplant
HT **Hakan Tek** Commented 3 months ago
Für die Integration auf der .NET-Seite können wir die Pkcs11Interop-Bibliothek und die entsprechende DevExpress-Infrastruktur verwenden. @OlgunR, falls du noch weitere Vorschläge hast, füge sie bitte hinzu. @Marvin Kamm, @Henning Emrich, @Matthias Dewald, um mit den Tests beginnen zu können, benötigen wir die entsprechenden Verbindungsdaten für SoftHSM (Slot-ID, PIN, Bibliothekspfad usw.).
FYI
@Marlon Schreiber @Jan-Ulrich Hoss
bugtracker.dd:8080/issue/SWINFRA-54/Test-SoftHSM
1/2
10/6/26, 10:46 PM
Test SoftHSM : SWINFRA-54
MD
## **Matthias Dewald** Commented 3 months ago
Hallo zusammen, SoftHSM ist auf der 172.24.12.64 installiert:
Der Proxy läuft auf Port 5657 und erwartet Verbindungen über die Proxy-Bibliothek (libpkcs11-proxy.so)
@Hakan Tek Bitte testen
MD
## **Matthias Dewald** Commented about 1 month ago
Ablauf SignFlow Unternehmenszertifikat.drawio.pdf
**PDF**
Ablauf SignFlow Un ternehme… 83 kB
bugtracker.dd:8080/issue/SWINFRA-54/Test-SoftHSM
2/2

View File

@@ -0,0 +1,22 @@
namespace EnvelopeGenerator.Server.Client.Components.Sender;
public sealed record CreateEnvelopeWizardResult(
string Title,
string Message,
int? EnvelopeTypeId,
string Language,
bool UseAccessCode,
bool TFAEnabled,
int CertificationType,
int FinalEmailToCreator,
int FinalEmailToReceivers,
bool SendReminderEmails,
int FirstReminderDays,
int ReminderIntervalDays,
int ExpiresWhenDays,
int ExpiresWarningWhenDays,
IReadOnlyList<CreateEnvelopeWizardReceiver> Receivers,
byte[]? DocumentBytes,
string? DocumentFileName);
public sealed record CreateEnvelopeWizardReceiver(string FullName, string EmailAddress);

View File

@@ -0,0 +1,104 @@
@using Microsoft.AspNetCore.Components.Forms
<div class="sender-pdf-dropzone @DropZoneClass">
<div class="sender-pdf-dropzone__content">
<div class="sender-pdf-dropzone__title">@CurrentActionText</div>
@if (!string.IsNullOrWhiteSpace(DragDropHintText))
{
<div class="sender-pdf-dropzone__hint">@DragDropHintText</div>
}
</div>
<InputFile OnChange="OnFileSelectedAsync"
accept="@Accept"
disabled="@Disabled"
@ondragenter="OnDragEnter"
@ondragleave="OnDragLeave"
@ondragover="OnDragOver"
@ondrop="OnDrop"
class="sender-pdf-dropzone__input" />
</div>
@code {
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public bool HasDocument { get; set; }
[Parameter]
public string Accept { get; set; } = ".pdf";
[Parameter]
public string UploadText { get; set; } = "PDF hochladen";
[Parameter]
public string ReplaceText { get; set; } = "PDF ersetzen";
[Parameter]
public string DragDropHintText { get; set; } = "Datei hierhin ziehen oder klicken";
[Parameter]
public EventCallback<InputFileChangeEventArgs> FileSelected { get; set; }
private int _dragDepth;
private bool _isDragActive;
private string CurrentActionText => HasDocument ? ReplaceText : UploadText;
private string DropZoneClass
{
get
{
if (Disabled)
return "sender-pdf-dropzone--disabled";
return _isDragActive ? "sender-pdf-dropzone--drag-active" : string.Empty;
}
}
private void OnDragEnter(DragEventArgs _)
{
if (Disabled)
return;
_dragDepth++;
_isDragActive = true;
}
private void OnDragLeave(DragEventArgs _)
{
if (Disabled)
return;
_dragDepth = Math.Max(0, _dragDepth - 1);
if (_dragDepth == 0)
_isDragActive = false;
}
private void OnDragOver(DragEventArgs _)
{
if (Disabled)
return;
_isDragActive = true;
}
private void OnDrop(DragEventArgs _)
{
_dragDepth = 0;
_isDragActive = false;
}
private async Task OnFileSelectedAsync(InputFileChangeEventArgs args)
{
_dragDepth = 0;
_isDragActive = false;
if (Disabled)
return;
if (FileSelected.HasDelegate)
await FileSelected.InvokeAsync(args);
}
}

View File

@@ -0,0 +1,25 @@
namespace EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFilter;
public readonly record struct ReceiverFilterCriteria
{
public readonly string FullName;
public readonly string EmailAddress;
public ReceiverFilterCriteria(string? fullName = "", string? emailAddress = "")
{
FullName = Normalize(fullName);
EmailAddress = Normalize(emailAddress);
}
public readonly bool Matches(ReceiverFilterCriteria criteria)
=> FullName.Contains(criteria.FullName, StringComparison.Ordinal)
&& EmailAddress.Contains(criteria.EmailAddress, StringComparison.Ordinal);
private static string Normalize(string? text)
{
if (string.IsNullOrWhiteSpace(text))
return string.Empty;
return string.Concat(text.Where(c => !char.IsWhiteSpace(c))).ToLowerInvariant();
}
}

View File

@@ -0,0 +1,26 @@
using EnvelopeGenerator.Application.Common.Dto.Receiver;
namespace EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFilter;
public class ReceiverSuggestionFilterHandler(IEnumerable<RecentReceiverSuggestionDto> suggestions)
{
private readonly IReadOnlyList<Entry> _entries = suggestions
.Where(x => !string.IsNullOrWhiteSpace(x.FullName) && !string.IsNullOrWhiteSpace(x.EmailAddress))
.Select(x => new Entry(new ReceiverFilterCriteria(x.FullName, x.EmailAddress), x))
.ToList();
private readonly record struct Entry(ReceiverFilterCriteria Criteria, RecentReceiverSuggestionDto Suggestion);
public IEnumerable<RecentReceiverSuggestionDto> Filter(ReceiverFilterCriteria criteria)
{
return _entries
.Where(x => x.Criteria.Matches(criteria))
.Select(x => x.Suggestion);
}
public IEnumerable<RecentReceiverSuggestionDto> Filter(string fullName, string emailAddress)
{
var criteria = new ReceiverFilterCriteria(fullName, emailAddress);
return Filter(criteria);
}
}

View File

@@ -80,7 +80,7 @@
_selected = option;
_isOpen = false;
await LocalizationService.SetLanguageAsync(option.Code);
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
}
private sealed record LanguageOption(string Code, string FlagClass, string ShortName);

View File

@@ -6,6 +6,7 @@
@using EnvelopeGenerator.Application.Envelopes.Commands
@using EnvelopeGenerator.Application.Resources
@using EnvelopeGenerator.Domain.Constants
@using EnvelopeGenerator.Domain.Resources
@using EnvelopeGenerator.Server.Client.Components.PdfEditor
@using EnvelopeGenerator.Server.Client.Services
@using Microsoft.AspNetCore.WebUtilities
@@ -20,76 +21,71 @@
@inject ReceiverService ReceiverService
@inject EnvelopeService EnvelopeService
@inject EnvelopeTypeService EnvelopeTypeService
@inject CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
@inject AuthService AuthService
@inject IStringLocalizer<Resource> UiLocalizer
@inject IStringLocalizer<Model> ModelLocalizer
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Viewer/css/dx-blazor-viewer-components.bs5.css" rel="stylesheet" />
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
<DxLoadingPanel Visible="@IsBootstrapLoading"
IsContentBlocked="true"
ApplyBackgroundShading="true"
IndicatorAreaVisible="true"
Text='@L("SenderEditor_BootstrapLoadingHint", "Der Umschlag wird vorbereitet und die Daten werden uebernommen.")'>
<div class="envelope-viewer-layout">
@* ── Action Bar ── *@
<div class="envelope-action-bar">
<div class="envelope-action-bar__inner"
style="flex-direction: row; align-items: center; padding: 0.35rem 1.5rem; gap: 0.75rem;">
<div class="envelope-action-bar__inner sender-editor-action-inner">
@* Left: Title + meta inputs + receivers *@
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem;">
<div class="sender-editor-main-column">
@* ── Title & Message inputs ── *@
<div style="display: flex; align-items: flex-start; gap: 0.5rem; flex-wrap: wrap;">
<div class="sender-editor-meta-row">
@* Titel *@
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 1 1 180px; min-width: 140px;">
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
text-transform: uppercase; letter-spacing: 0.05em;">
@L("Title", "Titel") <span style="color:#dc2626;">*</span>
<div class="sender-editor-input-field sender-editor-input-field--title">
<label class="sender-editor-label">
@L("Title", "Titel") <span class="sender-editor-required-asterisk">*</span>
</label>
<input type="text"
value="@_envelopeTitle"
@oninput="e => _envelopeTitle = e.Value?.ToString() ?? string.Empty"
placeholder='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
maxlength="120"
style="font-size: 0.78rem; font-weight: 500; color: #1f2937;
border: 1px solid #d1d5db; border-radius: 5px;
padding: 0.3rem 0.55rem; outline: none; width: 100%;
background: #fff;
@(_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle) ? "border-color:#dc2626;" : "")" />
class="sender-editor-text-input @(_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle) ? "sender-editor-text-input--invalid" : string.Empty)" />
@if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle))
{
<span style="font-size:0.6rem;color:#dc2626;">@L("RequiredField", "Pflichtfeld")</span>
<span class="sender-editor-required-text">@L("RequiredField", "Pflichtfeld")</span>
}
</div>
@* Nachricht *@
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 2 1 260px; min-width: 180px;">
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
text-transform: uppercase; letter-spacing: 0.05em;">
@L("Message", "Nachricht") <span style="color:#6b7280;font-weight:400;">(@L("Optional", "optional"))</span>
<div class="sender-editor-input-field sender-editor-input-field--message">
<label class="sender-editor-label">
@L("Message", "Nachricht") <span class="sender-editor-optional">(@L("Optional", "optional"))</span>
</label>
<input type="text"
value="@_envelopeMessage"
@oninput="e => _envelopeMessage = e.Value?.ToString() ?? string.Empty"
placeholder='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
maxlength="500"
style="font-size: 0.78rem; color: #1f2937;
border: 1px solid #d1d5db; border-radius: 5px;
padding: 0.3rem 0.55rem; outline: none; width: 100%;
background: #fff;" />
class="sender-editor-text-input" />
</div>
@* PDF filename badge *@
@if (_pdfLoaded)
{
<div style="display:flex;align-items:flex-end;gap:0.4rem;padding-bottom:0.05rem;flex-shrink:0;">
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
<div class="sender-editor-file-meta">
<span class="sender-editor-file-name">@_fileName</span>
@if (_signatureFields.Count > 0)
{
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
<span class="sender-editor-signature-count">
@_signatureFields.Count @L("SenderEditor_SignatureField", "Signaturfeld")@(_signatureFields.Count != 1 ? "er" : "")
</span>
}
@@ -97,106 +93,107 @@
}
</div>
<details style="border:1px solid #dbe5ef;border-radius:8px;background:#f9fbfd;">
<summary style="cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;padding:0.55rem 0.7rem;font-size:0.74rem;font-weight:700;color:#1f2937;text-transform:uppercase;letter-spacing:0.04em;">
<span>@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")</span>
<span style="font-size:0.66rem;color:#64748b;font-weight:600;">@L("SenderEditor_AdvancedOptional", "Erweitert")</span>
</summary>
<div class="sender-settings-shell">
<DxAccordion ExpandMode="AccordionExpandMode.SingleOrNone" CssClass="sender-settings-accordion">
<Items>
<DxAccordionItem Text='@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")' Expanded="false">
<ContentTemplate>
<div class="sender-settings-body">
<div class="sender-settings-grid">
<div class="sender-settings-field">
<label class="sender-settings-label">@L("EnvelopeType", "Umschlagtyp")</label>
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
@onchange="OnEnvelopeTypeChanged"
class="sender-settings-select">
<option value="">@L("SenderEditor_SelectTemplate", "Vorlage auswählen")</option>
@foreach (var type in _envelopeTypes)
{
<option value="@type.Id">@type.Title</option>
}
</select>
</div>
<div style="padding:0 0.7rem 0.7rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:0.45rem;">
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("EnvelopeType", "Umschlagtyp")</label>
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
@onchange="OnEnvelopeTypeChanged"
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
<option value="">@L("SenderEditor_SelectTemplate", "Vorlage auswählen")</option>
@foreach (var type in _envelopeTypes)
{
<option value="@type.Id">@type.Title</option>
}
</select>
</div>
<div class="sender-settings-field">
<label class="sender-settings-label">@L("Language", "Sprache")</label>
<select @bind="_language" class="sender-settings-select">
<option value="de">Deutsch (de)</option>
<option value="en">English (en)</option>
<option value="de-DE">Deutsch (de-DE)</option>
<option value="en-US">English (en-US)</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("Language", "Sprache")</label>
<select @bind="_language"
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
<option value="de">Deutsch (de)</option>
<option value="en">English (en)</option>
<option value="de-DE">Deutsch (de-DE)</option>
<option value="en-US">English (en-US)</option>
</select>
</div>
<div class="sender-settings-field">
<label class="sender-settings-label">@L("CertificationType", "Zertifizierung")</label>
<select @bind="_certificationType" class="sender-settings-select">
<option value="1">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
<option value="2">@L("ElectronicSignature", "Electronic Signature")</option>
<option value="3">@L("QualifiedSignature", "Qualified Signature")</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("CertificationType", "Zertifizierung")</label>
<select @bind="_certificationType"
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
<option value="1">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
<option value="2">@L("ElectronicSignature", "Electronic Signature")</option>
<option value="3">@L("QualifiedSignature", "Qualified Signature")</option>
</select>
</div>
<div class="sender-settings-toggles">
<label class="sender-settings-toggle">
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
@L("TFAEnabled", "2FA aktiviert")
</label>
<label class="sender-settings-toggle">
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
@L("UseAccessCode", "Zugriffscode verwenden")
</label>
</div>
<div style="display:flex;flex-direction:column;gap:0.3rem;justify-content:flex-end;padding-bottom:0.25rem;">
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
@L("TFAEnabled", "2FA aktiviert")
</label>
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
@L("UseAccessCode", "Zugriffscode verwenden")
</label>
</div>
<div class="sender-settings-field">
<label class="sender-settings-label">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
<select @bind="_finalEmailToCreator" class="sender-settings-select">
<option value="0">@L("No", "Nein")</option>
<option value="1">@L("Yes", "Ja")</option>
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
<select @bind="_finalEmailToCreator"
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
<option value="0">@L("No", "Nein")</option>
<option value="1">@L("Yes", "Ja")</option>
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
</select>
</div>
<div class="sender-settings-field">
<label class="sender-settings-label">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
<select @bind="_finalEmailToReceivers" class="sender-settings-select">
<option value="0">@L("No", "Nein")</option>
<option value="1">@L("Yes", "Ja")</option>
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
<select @bind="_finalEmailToReceivers"
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
<option value="0">@L("No", "Nein")</option>
<option value="1">@L("Yes", "Ja")</option>
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
</select>
</div>
<div class="sender-settings-toggles sender-settings-toggles--single">
<label class="sender-settings-toggle">
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
</label>
</div>
<div style="display:flex;flex-direction:column;gap:0.3rem;justify-content:flex-end;padding-bottom:0.25rem;">
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
</label>
</div>
<div class="sender-settings-field">
<label class="sender-settings-label">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm sender-settings-number" disabled="@(!_sendReminderEmails)" />
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
</div>
<div class="sender-settings-field">
<label class="sender-settings-label">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm sender-settings-number" disabled="@(!_sendReminderEmails)" />
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
</div>
<div class="sender-settings-field">
<label class="sender-settings-label">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm sender-settings-number" />
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm" />
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm" />
</div>
</div>
</details>
<div class="sender-settings-field">
<label class="sender-settings-label">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm sender-settings-number" />
</div>
</div>
</div>
</ContentTemplate>
</DxAccordionItem>
</Items>
</DxAccordion>
</div>
<div class="sender-receivers-panel">
<div class="sender-receivers-panel__header">
@@ -226,8 +223,7 @@
<div class="sender-receivers-list">
@foreach (var receiver in _receivers)
{
<div class="sender-receiver-chip"
style="border-left: 3px solid @receiver.Color;">
<div class="sender-receiver-chip" style="--receiver-color:@receiver.Color;">
<div class="sender-receiver-chip__body">
<div class="sender-receiver-chip__name">@receiver.FullName</div>
<div class="sender-receiver-chip__email">@receiver.Email</div>
@@ -237,11 +233,11 @@
}
</div>
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "pdf-toolbar__btn--signature-change-active" : "")"
@onclick="() => ActivatePlacementForReceiver(receiver)"
style="@(_pendingReceiverForPlacement?.Id == receiver.Id ? $"background:{receiver.Color};color:#fff;border-color:{receiver.Color};" : $"color:{receiver.Color};border-color:{receiver.Color};")"
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__place-btn
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "sender-receiver-chip__place-btn--active" : "sender-receiver-chip__place-btn--idle")"
@onclick="() => ActivatePlacementForReceiver(receiver)"
style="--receiver-color:@receiver.Color;"
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
</svg>
@@ -255,7 +251,7 @@
</div>
@* Right: Buttons *@
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;">
<div class="sender-editor-actions">
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
@onclick="Cancel"
@@ -264,14 +260,12 @@
</button>
@* PDF Upload *@
<label class="pdf-toolbar__btn"
<label class="pdf-toolbar__btn sender-editor-upload-btn @((!_isPageReady || _isPdfUploadInProgress) ? "sender-editor-upload-btn--disabled" : string.Empty)"
title='@L("UploadPdf", "PDF hochladen")'
style="cursor: @((_isPageReady && !_isPdfUploadInProgress) ? "pointer" : "not-allowed");
opacity:@((_isPageReady && !_isPdfUploadInProgress) ? "1" : "0.6");
display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.3rem 0.75rem; font-size: 0.75rem; font-weight: 500;">
>
@if (_isPdfUploadInProgress)
{
<span class="spinner-border spinner-border-sm" role="status" style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
<span class="spinner-border spinner-border-sm sender-editor-spinner" role="status"></span>
}
else
{
@@ -299,7 +293,7 @@
<InputFile OnChange="OnPdfFileSelectedAsync"
accept=".pdf"
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
style="display: none;" />
class="sender-editor-hidden-input" />
</label>
@if (_pdfLoaded && _signatureFields.Count > 0)
@@ -320,8 +314,7 @@
title='@L("Save", "Speichern")'>
@if (_isSaving)
{
<span class="spinner-border spinner-border-sm me-1" role="status"
style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
}
else
{
@@ -336,12 +329,10 @@
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
@onclick="SendAsync"
disabled="@_isSaving"
title='@L("SendEnvelope", "Umschlag senden")'
style="background: linear-gradient(135deg,#059669,#047857); color:#fff; border-radius:6px; padding:0.3rem 0.75rem; font-size:0.75rem; font-weight:600; border:none;">
title='@L("SendEnvelope", "Umschlag senden")'>
@if (_isSaving)
{
<span class="spinner-border spinner-border-sm me-1" role="status"
style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
}
else
{
@@ -357,11 +348,9 @@
@* Placement mode hint bar *@
@if (_pendingReceiverForPlacement is not null)
{
<div style="background: @_pendingReceiverForPlacement.Color; color: white; font-size: 0.75rem; font-weight: 500;
padding: 0.3rem 1.5rem; text-align: center; letter-spacing: 0.01em;">
<div class="sender-editor-placement-hint" style="--placement-color:@_pendingReceiverForPlacement.Color;">
📌 @L("SenderEditor_ClickDocumentFor", "Klicken Sie auf die Stelle im Dokument für") <strong>@_pendingReceiverForPlacement.FullName</strong>.
&nbsp;<button @onclick="CancelPlacement"
style="background:none;border:none;color:white;cursor:pointer;font-size:0.75rem;text-decoration:underline;padding:0;">
&nbsp;<button @onclick="CancelPlacement" class="sender-editor-placement-cancel">
@L("Cancel", "Abbrechen")
</button>
</div>
@@ -369,30 +358,25 @@
</div>
@* ── Content ── *@
<div class="envelope-content" style="padding: 0; overflow: hidden;">
<div class="envelope-content sender-editor-content">
@if (!_pdfLoaded)
{
@* Empty state *@
<div class="d-flex justify-content-center align-items-center h-100">
<div class="text-center" style="max-width: 420px;">
<div style="width: 72px; height: 72px; background: rgba(255,255,255,0.15);
border-radius: 50%; display: flex; align-items: center;
justify-content: center; margin: 0 auto 1.25rem;">
<div class="text-center sender-editor-empty-state">
<div class="sender-editor-empty-state__icon-wrap">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="white" viewBox="0 0 16 16">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
</svg>
</div>
<h5 class="text-white mb-2">@L("SenderEditor_NoDocumentLoaded", "Kein Dokument geladen")</h5>
<p class="text-white mb-4" style="opacity: 0.75; font-size: 0.85rem;">
<p class="text-white mb-4 sender-editor-empty-state__description">
@L("SenderEditor_UploadPdfToPlaceFields", "Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.")
</p>
<label class="btn btn-light btn-sm"
style="cursor: @((_isPageReady && !_isPdfUploadInProgress) ? "pointer" : "not-allowed");
opacity:@((_isPageReady && !_isPdfUploadInProgress) ? "1" : "0.8");
font-weight: 500;">
<label class="btn btn-light btn-sm sender-editor-empty-state__upload @((!_isPageReady || _isPdfUploadInProgress) ? "sender-editor-empty-state__upload--disabled" : string.Empty)">
@if (_isPdfUploadInProgress)
{
<span class="spinner-border spinner-border-sm me-1" role="status" style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
}
else
{
@@ -420,7 +404,7 @@
<InputFile OnChange="OnPdfFileSelectedAsync"
accept=".pdf"
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
style="display: none;" />
class="sender-editor-hidden-input" />
</label>
</div>
</div>
@@ -447,6 +431,7 @@
}
</div>
</div>
</DxLoadingPanel>
<DxPopup @bind-Visible="_receiverPopupVisible"
HeaderText='@L("AddReceiver", "Empfänger hinzufügen")'
@@ -544,18 +529,17 @@
<BodyContentTemplate>
@if (_saveErrorMessage is null)
{
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
<div style="flex-shrink:0;width:40px;height:40px;background:#d1fae5;border-radius:50%;
display:flex;align-items:center;justify-content:center;">
<div class="sender-editor-save-result">
<div class="sender-editor-save-result__icon sender-editor-save-result__icon--success">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</svg>
</div>
<div>
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
<p class="sender-editor-save-result__title">
@_saveSuccessTitle
</p>
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;">
<p class="sender-editor-save-result__message">
@_saveSuccessBody
</p>
</div>
@@ -563,19 +547,18 @@
}
else
{
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
<div style="flex-shrink:0;width:40px;height:40px;background:#fee2e2;border-radius:50%;
display:flex;align-items:center;justify-content:center;">
<div class="sender-editor-save-result">
<div class="sender-editor-save-result__icon sender-editor-save-result__icon--error">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#991b1b" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
</svg>
</div>
<div>
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
<p class="sender-editor-save-result__title">
@L("SenderEditor_ErrorCreatingEnvelope", "Fehler beim Erstellen des Umschlags")
</p>
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;word-break:break-word;">
<p class="sender-editor-save-result__message sender-editor-save-result__message--break">
@_saveErrorMessage
</p>
</div>
@@ -583,30 +566,25 @@
}
</BodyContentTemplate>
<FooterContentTemplate>
<div style="display:flex;gap:0.5rem;justify-content:flex-end;width:100%;padding:0.5rem 0;">
<div class="sender-editor-save-actions">
@if (_saveErrorMessage is null)
{
<button class="btn btn-outline-secondary"
@onclick="() => _savePopupVisible = false"
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
<button class="btn btn-outline-secondary sender-editor-save-actions__secondary"
@onclick="() => _savePopupVisible = false">
@L("Close", "Schließen")
</button>
@if (_saveNavigateToDashboard)
{
<button class="btn btn-primary"
@onclick="GoToDashboard"
style="background:linear-gradient(135deg,#059669,#047857);border:none;border-radius:6px;
padding:0.5rem 1.5rem;font-weight:600;color:#fff;">
<button class="btn btn-primary sender-editor-save-actions__primary" @onclick="GoToDashboard">
@L("SenderEditor_ToOverview", "Zur Übersicht")
</button>
}
}
else
{
<button class="btn btn-outline-secondary"
@onclick="() => _savePopupVisible = false"
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
<button class="btn btn-outline-secondary sender-editor-save-actions__secondary"
@onclick="() => _savePopupVisible = false">
@L("Close", "Schließen")
</button>
}
@@ -618,6 +596,9 @@
[SupplyParameterFromQuery(Name = "envelopeId")]
public int? EnvelopeId { get; set; }
[SupplyParameterFromQuery(Name = "bootstrapToken")]
public string? BootstrapToken { get; set; }
// ── Constants ──
// Signature field size in PDF points (fixed): 1.77" × 1.96"
const double SigWidthPt = 1.77 * 72; // 127.44 pt
@@ -638,6 +619,8 @@
int? _loadedEnvelopeId;
int? _editingEnvelopeId;
bool _isLoadingEnvelopeForEdit;
bool _bootstrapApplied;
string? _pendingBootstrapToken;
List<SignatureFieldDraft> _signatureFields = [];
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
@@ -658,6 +641,7 @@
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
string _saveSuccessBody = string.Empty;
bool _saveNavigateToDashboard = false;
bool _saveDraftAfterBootstrap;
// ── Envelope metadata ──
string _envelopeTitle = string.Empty;
@@ -692,6 +676,7 @@
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
bool IsEditMode => EnvelopeId.HasValue && EnvelopeId.Value > 0;
bool IsBootstrapLoading => !IsEditMode && !_bootstrapApplied;
// ── Lifecycle ──
@@ -732,19 +717,115 @@
return;
}
if (IsEditMode)
if (IsEditMode || _bootstrapApplied)
return;
if (string.IsNullOrWhiteSpace(BootstrapToken))
{
NavigationManager.NavigateTo("/sender", replace: true);
return;
}
_pendingBootstrapToken = BootstrapToken;
}
protected override Task OnAfterRenderAsync(bool firstRender)
async Task TryApplyBootstrapAsync(string token)
{
var payload = await CreateEnvelopeBootstrapStorageService.ConsumeAsync(token);
_bootstrapApplied = true;
if (payload is null)
{
NavigationManager.NavigateTo("/sender", replace: true);
return;
}
_envelopeTitle = payload.Title ?? string.Empty;
_envelopeMessage = payload.Message ?? string.Empty;
_selectedEnvelopeTypeId = payload.EnvelopeTypeId;
_language = string.IsNullOrWhiteSpace(payload.Language) ? "de-DE" : payload.Language;
_useAccessCode = payload.UseAccessCode;
_tfaEnabled = payload.TFAEnabled;
_certificationType = payload.CertificationType;
_finalEmailToCreator = payload.FinalEmailToCreator;
_finalEmailToReceivers = payload.FinalEmailToReceivers;
_sendReminderEmails = payload.SendReminderEmails;
_firstReminderDays = payload.FirstReminderDays;
_reminderIntervalDays = payload.ReminderIntervalDays;
_expiresWhenDays = payload.ExpiresWhenDays;
_expiresWarningWhenDays = payload.ExpiresWarningWhenDays;
NormalizeSettings();
_receivers = payload.Receivers
.Select((receiver, index) => new ReceiverDraft(
Guid.NewGuid(),
receiver.FullName,
receiver.EmailAddress,
string.Empty,
ReceiverPalette[index % ReceiverPalette.Length]))
.ToList();
_originalPdfBytes = payload.DocumentBytes;
_fileName = string.IsNullOrWhiteSpace(payload.DocumentFileName) ? "document.pdf" : payload.DocumentFileName;
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
_signatureFields.Clear();
_pendingReceiverForPlacement = null;
_saveDraftAfterBootstrap = true;
if (_pdfLoaded && _originalPdfBytes is not null)
{
try
{
_pagePreviews = (await PdfRenderService.RenderPagesAsync(_originalPdfBytes))
.OrderBy(p => p.PageNumber)
.ToList();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to render bootstrap PDF previews.");
_pagePreviews = [];
}
_pageCount = _pagePreviews.Count > 0
? _pagePreviews.Count
: EstimatePdfPageCount(_originalPdfBytes) ?? 1;
}
else
{
_pagePreviews = [];
_pageCount = 0;
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_isPageReady = true;
StateHasChanged();
await InvokeAsync(StateHasChanged);
}
if (!OperatingSystem.IsBrowser())
return;
if (!IsEditMode && !_bootstrapApplied && !string.IsNullOrWhiteSpace(_pendingBootstrapToken))
{
var token = _pendingBootstrapToken;
_pendingBootstrapToken = null;
await TryApplyBootstrapAsync(token);
await InvokeAsync(StateHasChanged);
return;
}
if (_saveDraftAfterBootstrap && !_isSaving)
{
_saveDraftAfterBootstrap = false;
await SaveDraftAndStayOnEditorAsync();
return;
}
return Task.CompletedTask;
}
async Task LoadEnvelopeForEditAsync(int envelopeId)
@@ -1137,12 +1218,17 @@
}
async Task SaveDraftAsync()
{
await SaveDraftCoreAsync(navigateToDashboard: true);
}
async Task<int?> SaveDraftCoreAsync(bool navigateToDashboard)
{
if (!ValidateSettings(out var settingsError))
{
_saveErrorMessage = settingsError;
_savePopupVisible = true;
return;
return null;
}
_isSaving = true;
@@ -1163,25 +1249,37 @@
_loadedEnvelopeId = result.Id;
_editingEnvelopeId = result.Id;
if (previousEnvelopeId != result.Id)
if (navigateToDashboard)
{
NavigationManager.NavigateTo("/sender", forceLoad: false);
}
else if (previousEnvelopeId != result.Id)
{
NavigationManager.NavigateTo($"/sender/editor?envelopeId={result.Id}", forceLoad: false);
}
NavigationManager.NavigateTo("/sender", forceLoad: false);
return result.Id;
}
catch (Exception ex)
{
Logger.LogError(ex, "[SenderEditor] Failed to save draft");
_saveErrorMessage = ex.Message;
_savePopupVisible = true;
return null;
}
finally
{
_isSaving = false;
await InvokeAsync(StateHasChanged);
}
}
async Task SaveDraftAndStayOnEditorAsync()
{
Logger.LogInformation("[SenderEditor] Trigger bootstrap draft save.");
await SaveDraftCoreAsync(navigateToDashboard: false);
}
// ── Send envelope ──
async Task SendAsync()
{
@@ -1586,7 +1684,11 @@
string L(string key, string fallback)
{
var value = UiLocalizer[key];
return value.ResourceNotFound ? fallback : value.Value;
var modelValue = ModelLocalizer[key];
if (!modelValue.ResourceNotFound)
return modelValue.Value;
var uiValue = UiLocalizer[key];
return uiValue.ResourceNotFound ? fallback : uiValue.Value;
}
}

View File

@@ -9,15 +9,18 @@
@using EnvelopeGenerator.Application.Common.Dto.History
@using EnvelopeGenerator.Application.Resources
@using EnvelopeGenerator.Domain.Resources
@using EnvelopeGenerator.Server.Client.Components.Sender
@using Microsoft.Extensions.Localization
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
@inject EnvelopeGenerator.Server.Client.Services.DocumentService DocumentService
@inject EnvelopeGenerator.Server.Client.Services.PdfRenderService PdfRenderService
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeTypeService EnvelopeTypeService
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
@inject ILogger<EnvelopeSenderPage> Logger
@inject EnvelopeGenerator.Server.Client.Services.CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
@inject ILogger<SenderPage> Logger
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime
@inject AppVersionService AppVersion
@@ -414,6 +417,11 @@
</div>
<CreateEnvelopeWizardPopup Visible="@_createWizardVisible"
VisibleChanged="OnCreateWizardVisibleChangedAsync"
EnvelopeTypes="@_envelopeTypes"
StartEditorRequested="StartEditorFromWizardAsync" />
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
<DxPopup @bind-Visible="@_previewVisible"
HeaderText="@($"{L("Preview", "Vorschau")} — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
@@ -567,6 +575,9 @@
private string _deleteReason = string.Empty;
private string? _deleteErrorMessage;
private bool _createWizardVisible = false;
private IReadOnlyList<EnvelopeTypeDto> _envelopeTypes = [];
// Stable keys used to store/restore each tab's layout in UserPreferences.GridLayouts
private const string GridKeyActive = "sender.active-envelopes";
private const string GridKeyCompleted = "sender.completed-envelopes";
@@ -596,6 +607,16 @@
_appLogo = logos.App;
_companyLogo = logos.Company;
try
{
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to load envelope types for create wizard.");
_envelopeTypes = [];
}
await LoadEnvelopesAsync();
}
@@ -693,7 +714,40 @@
void CreateEnvelope()
{
Navigation.NavigateTo("/sender/editor");
_createWizardVisible = true;
}
Task OnCreateWizardVisibleChangedAsync(bool visible)
{
_createWizardVisible = visible;
return Task.CompletedTask;
}
async Task StartEditorFromWizardAsync(CreateEnvelopeWizardResult result)
{
var payload = new CreateEnvelopeBootstrapPayload(
Title: result.Title,
Message: result.Message,
EnvelopeTypeId: result.EnvelopeTypeId,
Language: result.Language,
UseAccessCode: result.UseAccessCode,
TFAEnabled: result.TFAEnabled,
CertificationType: result.CertificationType,
FinalEmailToCreator: result.FinalEmailToCreator,
FinalEmailToReceivers: result.FinalEmailToReceivers,
SendReminderEmails: result.SendReminderEmails,
FirstReminderDays: result.FirstReminderDays,
ReminderIntervalDays: result.ReminderIntervalDays,
ExpiresWhenDays: result.ExpiresWhenDays,
ExpiresWarningWhenDays: result.ExpiresWarningWhenDays,
Receivers: result.Receivers
.Select(x => new CreateEnvelopeBootstrapReceiver(x.FullName, x.EmailAddress))
.ToList(),
DocumentBytes: result.DocumentBytes,
DocumentFileName: result.DocumentFileName);
var token = await CreateEnvelopeBootstrapStorageService.StoreAsync(payload);
Navigation.NavigateTo($"/sender/editor?bootstrapToken={token}");
}
void EditEnvelope()

View File

@@ -45,6 +45,7 @@ builder.Services.AddScoped<EnvelopeTypeService>();
builder.Services.AddScoped<LocalizationService>();
builder.Services.AddScoped<ReceiverService>();
builder.Services.AddScoped<PdfRenderService>();
builder.Services.AddScoped<CreateEnvelopeBootstrapStorageService>();
// UI Preferences & Theme
builder.Services.AddScoped<UserPreferencesService>();

View File

@@ -0,0 +1,64 @@
using System.Text.Json;
using Microsoft.JSInterop;
namespace EnvelopeGenerator.Server.Client.Services;
public sealed class CreateEnvelopeBootstrapStorageService(IJSRuntime jsRuntime)
{
private const string StorageKeyPrefix = "sender.createEnvelope.bootstrap.";
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
public async Task<string> StoreAsync(CreateEnvelopeBootstrapPayload payload)
{
var token = Guid.NewGuid().ToString("N");
var key = BuildStorageKey(token);
var json = JsonSerializer.Serialize(payload, JsonOptions);
await jsRuntime.InvokeVoidAsync("sessionStorage.setItem", key, json);
return token;
}
public async Task<CreateEnvelopeBootstrapPayload?> ConsumeAsync(string? token)
{
if (string.IsNullOrWhiteSpace(token))
return null;
var key = BuildStorageKey(token);
var raw = await jsRuntime.InvokeAsync<string?>("sessionStorage.getItem", key);
await jsRuntime.InvokeVoidAsync("sessionStorage.removeItem", key);
if (string.IsNullOrWhiteSpace(raw))
return null;
try
{
return JsonSerializer.Deserialize<CreateEnvelopeBootstrapPayload>(raw, JsonOptions);
}
catch
{
return null;
}
}
private static string BuildStorageKey(string token) => $"{StorageKeyPrefix}{token}";
}
public sealed record CreateEnvelopeBootstrapPayload(
string Title,
string Message,
int? EnvelopeTypeId,
string Language,
bool UseAccessCode,
bool TFAEnabled,
int CertificationType,
int FinalEmailToCreator,
int FinalEmailToReceivers,
bool SendReminderEmails,
int FirstReminderDays,
int ReminderIntervalDays,
int ExpiresWhenDays,
int ExpiresWarningWhenDays,
IReadOnlyList<CreateEnvelopeBootstrapReceiver> Receivers,
byte[]? DocumentBytes,
string? DocumentFileName);
public sealed record CreateEnvelopeBootstrapReceiver(string FullName, string EmailAddress);

View File

@@ -1,5 +1,6 @@
using System.Net.Http.Json;
using System.Text.Json;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
namespace EnvelopeGenerator.Server.Client.Services;
@@ -26,4 +27,21 @@ public class ReceiverService(IHttpClientFactory httpClientFactory)
var emails = await response.Content.ReadFromJsonAsync<List<string>>(JsonOptions, cancel);
return emails ?? [];
}
/// <summary>
/// Fetches recent receiver suggestions for the authenticated sender.
/// Calls GET /api/EnvelopeReceiver/recent-suggestions?take={take}.
/// </summary>
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> GetRecentReceiverSuggestionsAsync(int take = 20, CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var url = $"/api/EnvelopeReceiver/recent-suggestions?take={take}";
var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode)
return [];
var suggestions = await response.Content.ReadFromJsonAsync<List<RecentReceiverSuggestionDto>>(JsonOptions, cancel);
return suggestions ?? [];
}
}

View File

@@ -112,6 +112,20 @@ public class EnvelopeReceiverController : ControllerBase
return name is null ? NotFound() : Ok(name);
}
/// <summary>
/// Returns recent receiver suggestions for the authenticated sender.
/// </summary>
/// <param name="query">Suggestion query options (take 3..5 is applied in handler).</param>
/// <param name="cancel">Cancellation token.</param>
/// <returns>List of recent distinct receiver suggestions.</returns>
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
[HttpGet("recent-suggestions")]
public async Task<IActionResult> GetRecentSuggestions([FromQuery] ReadRecentReceiverSuggestionsQuery query, CancellationToken cancel)
{
var result = await _mediator.Send(query.ForSender(User.GetUsername()), cancel);
return Ok(result);
}
/// <summary>
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
/// </summary>
@@ -278,4 +292,4 @@ public class EnvelopeReceiverController : ControllerBase
}
}
}
}

View File

@@ -346,6 +346,7 @@ try
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.ReceiverService>();
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.PdfRenderService>();
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.EnvelopeTypeService>();
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.CreateEnvelopeBootstrapStorageService>();
// Business Services (Server specific)
builder.Services.AddScoped<DocumentService>();

View File

@@ -1002,6 +1002,108 @@ body.resizing {
border: 1px solid rgba(126, 34, 206, 0.12);
}
.sender-settings-shell {
border-radius: 12px;
overflow: hidden;
}
.sender-settings-accordion {
border: 1px solid #dbe5ef;
border-radius: 12px;
background: linear-gradient(135deg, #ffffff 0%, #f8fbff 100%);
}
.sender-settings-accordion .dxbl-accordion-item {
border: none;
}
.sender-settings-accordion .dxbl-accordion-item-header {
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.66rem;
font-weight: 600;
color: #1f2937;
padding: 0.38rem 0.62rem;
background: rgba(124, 58, 237, 0.04);
line-height: 1.15;
}
.sender-settings-body {
display: flex;
flex-direction: column;
gap: 0.65rem;
padding: 0.25rem 0.7rem 0.7rem;
}
.sender-settings-chip {
align-self: flex-start;
font-size: 0.66rem;
font-weight: 700;
color: #5b21b6;
background: rgba(126, 34, 206, 0.1);
border: 1px solid rgba(126, 34, 206, 0.15);
border-radius: 999px;
padding: 0.12rem 0.5rem;
}
.sender-settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.5rem;
}
.sender-settings-field {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.sender-settings-label {
font-size: 0.65rem;
font-weight: 700;
color: #64748b;
letter-spacing: 0.01em;
}
.sender-settings-select,
.sender-settings-number {
font-size: 0.78rem;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 0.3rem 0.45rem;
background: #fff;
min-height: 32px;
color: #1f2937;
}
.sender-settings-number:focus,
.sender-settings-select:focus {
border-color: rgba(126, 34, 206, 0.45);
box-shadow: 0 0 0 2px rgba(126, 34, 206, 0.12);
outline: none;
}
.sender-settings-toggles {
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 0.3rem;
padding-bottom: 0.1rem;
}
.sender-settings-toggle {
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-size: 0.74rem;
color: #1f2937;
min-height: 32px;
}
.sender-settings-toggles--single {
justify-content: center;
}
.sender-receivers-panel__header {
display: flex;
align-items: center;
@@ -1119,9 +1221,275 @@ body.resizing {
border-radius: 12px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(126, 34, 206, 0.12);
border-left: 3px solid var(--receiver-color, #7c3aed);
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
}
.sender-receiver-chip__place-btn--idle {
color: var(--receiver-color, #7c3aed);
border-color: var(--receiver-color, #7c3aed);
}
.sender-receiver-chip__place-btn--active {
background: var(--receiver-color, #7c3aed);
color: #ffffff;
border-color: var(--receiver-color, #7c3aed);
}
.sender-editor-action-inner {
flex-direction: row;
align-items: center;
padding: 0.35rem 1.5rem;
gap: 0.75rem;
}
.sender-editor-main-column {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.sender-editor-meta-row {
display: flex;
align-items: flex-start;
gap: 0.5rem;
flex-wrap: wrap;
}
.sender-editor-input-field {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.sender-editor-input-field--title {
flex: 1 1 180px;
min-width: 140px;
}
.sender-editor-input-field--message {
flex: 2 1 260px;
min-width: 180px;
}
.sender-editor-label {
font-size: 0.65rem;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sender-editor-required-asterisk {
color: #dc2626;
}
.sender-editor-optional {
color: #6b7280;
font-weight: 400;
}
.sender-editor-text-input {
font-size: 0.78rem;
color: #1f2937;
border: 1px solid #d1d5db;
border-radius: 5px;
padding: 0.3rem 0.55rem;
outline: none;
width: 100%;
background: #fff;
}
.sender-editor-text-input--invalid {
border-color: #dc2626;
}
.sender-editor-required-text {
font-size: 0.6rem;
color: #dc2626;
}
.sender-editor-file-meta {
display: flex;
align-items: flex-end;
gap: 0.4rem;
padding-bottom: 0.05rem;
flex-shrink: 0;
}
.sender-editor-file-name {
font-size: 0.7rem;
color: #6b7280;
}
.sender-editor-signature-count {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.4rem;
background: #ede9fe;
border-radius: 0.25rem;
color: #6d28d9;
font-weight: 500;
font-size: 0.7rem;
white-space: nowrap;
}
.sender-editor-actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.sender-editor-upload-btn {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.3rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
}
.sender-editor-upload-btn--disabled {
cursor: not-allowed;
opacity: 0.6;
}
.sender-editor-hidden-input {
display: none;
}
.sender-editor-spinner {
width: 0.8rem;
height: 0.8rem;
border-width: 2px;
}
.sender-editor-placement-hint {
background: var(--placement-color, #7c3aed);
color: #ffffff;
font-size: 0.75rem;
font-weight: 500;
padding: 0.3rem 1.5rem;
text-align: center;
letter-spacing: 0.01em;
}
.sender-editor-placement-cancel {
background: none;
border: none;
color: #ffffff;
cursor: pointer;
font-size: 0.75rem;
text-decoration: underline;
padding: 0;
}
.sender-editor-content {
padding: 0;
overflow: hidden;
}
.sender-editor-empty-state {
max-width: 420px;
}
.sender-editor-empty-state__icon-wrap {
width: 72px;
height: 72px;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.25rem;
}
.sender-editor-empty-state__description {
opacity: 0.75;
font-size: 0.85rem;
}
.sender-editor-empty-state__upload {
font-weight: 500;
cursor: pointer;
}
.sender-editor-empty-state__upload--disabled {
cursor: not-allowed;
opacity: 0.8;
}
.sender-editor-save-result {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 0.5rem 0;
}
.sender-editor-save-result__icon {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.sender-editor-save-result__icon--success {
background: #d1fae5;
}
.sender-editor-save-result__icon--error {
background: #fee2e2;
}
.sender-editor-save-result__title {
margin: 0 0 0.4rem;
font-weight: 600;
color: #1f2937;
font-size: 0.95rem;
}
.sender-editor-save-result__message {
margin: 0;
color: #6b7280;
font-size: 0.85rem;
line-height: 1.5;
}
.sender-editor-save-result__message--break {
word-break: break-word;
}
.sender-editor-save-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
width: 100%;
padding: 0.5rem 0;
}
.sender-editor-save-actions__secondary {
border-radius: 6px;
padding: 0.5rem 1.25rem;
font-weight: 500;
}
.sender-editor-save-actions__primary {
background: linear-gradient(135deg, #059669, #047857);
border: none;
border-radius: 6px;
padding: 0.5rem 1.5rem;
font-weight: 600;
color: #fff;
}
.sender-receiver-chip__body {
min-width: 0;
flex: 1;

View File

@@ -1094,3 +1094,557 @@
[data-sf-theme="blazing-dark"] .sender-preview-error-msg {
color: rgba(255, 255, 255, 0.50);
}
.sender-create-wizard {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.25rem 0;
outline: none;
}
.sender-create-wizard-popup .dxbl-popup-content {
max-height: min(78vh, 760px);
overflow: auto;
}
.sender-create-wizard-popup .dxbl-popup-footer {
position: sticky;
bottom: 0;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(6px);
z-index: 2;
}
.sender-create-wizard__progress {
position: relative;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
align-items: center;
gap: 0.5rem;
}
.sender-create-wizard__progress-track {
position: absolute;
left: 12px;
right: 12px;
top: 50%;
height: 2px;
transform: translateY(-50%);
background: rgba(126, 34, 206, 0.18);
z-index: 0;
}
.sender-create-wizard__step {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
}
.sender-create-wizard__step-index {
width: 24px;
height: 24px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.78rem;
font-weight: 700;
color: #64748b;
background: #f8fafc;
border: 1px solid rgba(126, 34, 206, 0.25);
}
.sender-create-wizard__step--active .sender-create-wizard__step-index {
color: #ffffff;
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
border-color: transparent;
box-shadow: 0 0 0 3px rgba(126, 34, 206, 0.15);
}
.sender-create-wizard__step--done .sender-create-wizard__step-index {
color: #ffffff;
background: #2a5298;
border-color: transparent;
}
.sender-create-wizard__meta {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.sender-create-wizard__title {
font-size: 1rem;
font-weight: 700;
color: #1f2937;
}
.sender-create-wizard__description {
font-size: 0.875rem;
color: #64748b;
line-height: 1.4;
}
.sender-create-wizard__stage {
border: 1px solid rgba(126, 34, 206, 0.12);
border-radius: 10px;
background: rgba(126, 34, 206, 0.04);
padding: 0.85rem 0.95rem;
}
.sender-create-wizard__stage p {
margin: 0;
color: #334155;
font-size: 0.9rem;
}
.sender-create-wizard__footer {
width: 100%;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
@media (max-width: 640px) {
.sender-create-wizard__footer {
flex-wrap: wrap;
}
.sender-create-wizard__footer .dxbl-btn {
flex: 1 1 calc(50% - 0.5rem);
}
}
.sender-create-wizard__validation {
border: 1px solid rgba(220, 38, 38, 0.24);
background: rgba(254, 226, 226, 0.9);
color: #991b1b;
border-radius: 8px;
padding: 0.6rem 0.75rem;
font-size: 0.85rem;
}
.sender-create-wizard__form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.9rem;
}
.sender-create-wizard__field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.sender-create-wizard__field--full {
grid-column: 1 / -1;
}
.sender-create-wizard__settings-grid {
display: grid;
gap: 0.8rem;
}
.sender-create-wizard__settings-group {
border: 1px solid rgba(148, 163, 184, 0.34);
border-radius: 10px;
background: rgba(248, 250, 252, 0.86);
padding: 0.8rem;
}
.sender-create-wizard__settings-title {
font-size: 0.8rem;
font-weight: 800;
color: #334155;
margin-bottom: 0.6rem;
}
.sender-create-wizard__settings-toggles {
display: flex;
flex-wrap: wrap;
gap: 0.55rem 1.1rem;
}
.sender-create-wizard__settings-toggle {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.82rem;
font-weight: 600;
color: #334155;
}
.sender-create-wizard__label {
font-size: 0.82rem;
font-weight: 700;
color: #334155;
}
.sender-create-wizard__required {
color: #dc2626;
}
.sender-create-wizard__optional {
color: #64748b;
font-weight: 600;
}
.sender-create-wizard__select {
min-height: 38px;
border: 1px solid rgba(126, 34, 206, 0.25);
border-radius: 8px;
padding: 0.45rem 0.55rem;
background: #ffffff;
color: #1f2937;
}
.sender-create-wizard__receiver-actions {
display: flex;
justify-content: flex-end;
}
.sender-create-wizard__add-receiver-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 36px;
min-width: 220px;
font-weight: 700;
font-size: 0.84rem;
border: 1px solid rgba(14, 116, 144, 0.28);
border-radius: 8px;
background: rgba(236, 254, 255, 0.95);
color: #0f766e;
box-shadow: 0 6px 14px rgba(14, 116, 144, 0.15);
padding: 0.45rem 0.9rem;
}
.sender-create-wizard__add-receiver-btn:hover:not(:disabled) {
background: rgba(207, 250, 254, 1);
color: #0f766e;
}
.sender-create-wizard__add-receiver-btn:disabled {
opacity: 0.58;
cursor: not-allowed;
}
.sender-create-wizard__suggestions {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.sender-create-wizard__suggestions-title {
font-size: 0.8rem;
font-weight: 700;
color: #475569;
}
.sender-create-wizard__suggestion-list {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
gap: 0.45rem;
padding-bottom: 0.25rem;
}
.sender-create-wizard__suggestion-item {
display: flex;
flex: 0 0 auto;
flex-direction: column;
align-items: flex-start;
gap: 0.12rem;
border: 1px solid rgba(14, 116, 144, 0.22);
border-radius: 999px;
background: rgba(236, 254, 255, 0.85);
color: #0f172a;
padding: 0.35rem 0.7rem;
min-width: 0;
}
.sender-create-wizard__receiver-tagbox-wrap {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.sender-create-wizard__receiver-tagbox {
width: 100%;
}
.sender-create-wizard__receiver-tag {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.18rem 0.45rem;
border-radius: 999px;
background: rgba(236, 254, 255, 0.95);
border: 1px solid rgba(14, 116, 144, 0.25);
}
.sender-create-wizard__receiver-tag-name {
font-weight: 700;
}
.sender-create-wizard__receiver-tag-email {
color: #64748b;
font-size: 0.78rem;
}
.sender-create-wizard__receiver-tag-remove {
border: 0;
background: transparent;
color: #0f766e;
font-size: 0.76rem;
font-weight: 700;
line-height: 1;
padding: 0;
width: 1rem;
height: 1rem;
cursor: pointer;
}
.sender-create-wizard__tagbox-empty {
height: 0;
padding: 0;
margin: 0;
}
.sender-create-wizard__suggestion-item:disabled {
opacity: 0.58;
}
.sender-create-wizard__suggestion-name {
font-size: 0.77rem;
font-weight: 700;
line-height: 1.1;
}
.sender-create-wizard__suggestion-email {
font-size: 0.72rem;
color: #155e75;
line-height: 1.1;
}
.sender-create-wizard__receiver-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.sender-create-wizard__receiver-item {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid rgba(126, 34, 206, 0.18);
border-radius: 8px;
padding: 0.55rem 0.7rem;
background: rgba(248, 250, 252, 0.95);
gap: 0.75rem;
}
.sender-create-wizard__receiver-name {
font-size: 0.86rem;
font-weight: 700;
color: #1f2937;
}
.sender-create-wizard__receiver-email {
font-size: 0.79rem;
color: #64748b;
}
.sender-create-wizard__receiver-remove {
border: 1px solid rgba(220, 38, 38, 0.2);
border-radius: 6px;
background: rgba(220, 38, 38, 0.08);
color: #b91c1c;
font-size: 0.78rem;
font-weight: 700;
padding: 0.25rem 0.55rem;
}
.sender-create-wizard__upload-stage {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.sender-create-wizard__upload-btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(126, 34, 206, 0.28);
border-radius: 8px;
padding: 0.55rem 0.9rem;
background: rgba(126, 34, 206, 0.08);
color: #4c1d95;
font-size: 0.84rem;
font-weight: 700;
width: fit-content;
cursor: pointer;
}
.sender-create-wizard__upload-btn--disabled {
opacity: 0.55;
cursor: not-allowed;
}
.sender-create-wizard__hidden-input {
display: none;
}
.sender-pdf-dropzone {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 92px;
border: 1px dashed rgba(100, 116, 139, 0.5);
border-radius: 8px;
background: rgba(248, 250, 252, 0.9);
color: #334155;
cursor: pointer;
transition: border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
}
.sender-pdf-dropzone:hover {
border-color: rgba(126, 34, 206, 0.45);
background: rgba(241, 245, 249, 0.95);
}
.sender-pdf-dropzone--drag-active {
border-color: rgba(16, 185, 129, 0.75);
background: linear-gradient(180deg, rgba(16, 185, 129, 0.14), rgba(16, 185, 129, 0.08));
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.16);
}
.sender-pdf-dropzone--disabled {
opacity: 0.55;
cursor: not-allowed;
}
.sender-pdf-dropzone__content {
text-align: center;
display: grid;
gap: 0.2rem;
padding: 0.75rem 0.9rem;
}
.sender-pdf-dropzone__title {
font-size: 0.86rem;
font-weight: 700;
}
.sender-pdf-dropzone__hint {
font-size: 0.78rem;
color: #64748b;
}
.sender-pdf-dropzone__input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.sender-pdf-dropzone--disabled .sender-pdf-dropzone__input {
cursor: not-allowed;
}
.sender-create-wizard__document-meta {
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 8px;
background: rgba(16, 185, 129, 0.08);
padding: 0.55rem 0.7rem;
}
.sender-create-wizard__document-preview-card {
display: flex;
flex-direction: column;
gap: 0.65rem;
}
.sender-create-wizard__document-preview-wrap {
border: 1px solid rgba(148, 163, 184, 0.38);
border-radius: 10px;
background: rgba(248, 250, 252, 0.96);
height: 320px;
overflow: hidden;
}
.sender-create-wizard__document-preview-wrap .sender-image-editor-shell {
height: 100%;
}
.sender-create-wizard__document-preview-wrap .sender-image-editor {
min-height: 0;
}
.sender-create-wizard__document-name {
color: #064e3b;
font-size: 0.85rem;
font-weight: 700;
}
.sender-create-wizard__document-size {
color: #065f46;
font-size: 0.77rem;
}
.sender-create-wizard__empty-list {
border: 1px dashed rgba(100, 116, 139, 0.4);
border-radius: 8px;
padding: 0.6rem 0.75rem;
color: #475569;
font-size: 0.83rem;
background: rgba(248, 250, 252, 0.8);
}
@media (max-width: 760px) {
.sender-create-wizard-popup .dxbl-popup {
width: 96vw !important;
max-width: 96vw !important;
}
.sender-create-wizard__progress {
gap: 0.35rem;
}
.sender-create-wizard__title {
font-size: 0.93rem;
}
.sender-create-wizard__description {
font-size: 0.81rem;
}
.sender-create-wizard__form-grid {
grid-template-columns: 1fr;
}
.sender-create-wizard__receiver-item {
flex-direction: column;
align-items: flex-start;
}
.sender-create-wizard__receiver-remove {
width: 100%;
}
.sender-pdf-dropzone {
width: 100%;
}
.sender-create-wizard__upload-btn {
width: 100%;
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -17,7 +17,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Bogus" Version="35.6.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
@@ -28,21 +28,60 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.20" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.20" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Bogus" Version="35.6.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.3.0" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.17">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure.Crypt\EnvelopeGenerator.Infrastructure.Crypt.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<Compile Remove="Application\Crypt\**\*.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.6" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>

View File

@@ -54,6 +54,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Service",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Infrastructure.Doc", "EnvelopeGenerator.Infrastructure.Doc\EnvelopeGenerator.Infrastructure.Doc.csproj", "{3B6EFE8D-F1EE-4666-87A0-9F90149CC61A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Infrastructure.Crypt", "EnvelopeGenerator.Infrastructure.Crypt\EnvelopeGenerator.Infrastructure.Crypt.csproj", "{B33A3B17-A4F8-4307-9C51-DDFE1B292DE7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -120,6 +122,10 @@ Global
{3B6EFE8D-F1EE-4666-87A0-9F90149CC61A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B6EFE8D-F1EE-4666-87A0-9F90149CC61A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B6EFE8D-F1EE-4666-87A0-9F90149CC61A}.Release|Any CPU.Build.0 = Release|Any CPU
{B33A3B17-A4F8-4307-9C51-DDFE1B292DE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B33A3B17-A4F8-4307-9C51-DDFE1B292DE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B33A3B17-A4F8-4307-9C51-DDFE1B292DE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B33A3B17-A4F8-4307-9C51-DDFE1B292DE7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -144,6 +150,7 @@ Global
{83ED2617-B398-4859-8F59-B38F8807E83E} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{6B87FB5F-F4EA-45BE-A218-385DC2CE8E80} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{3B6EFE8D-F1EE-4666-87A0-9F90149CC61A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{B33A3B17-A4F8-4307-9C51-DDFE1B292DE7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}