Compare commits

..

64 Commits

Author SHA1 Message Date
7456babe0d Merge branch 'master' into bugfix/devexpress-pdf-not-displaying 2026-06-24 16:17:40 +02:00
71e375d6ea Introduce SSR authentication service for EnvelopeReceiverPage
Replaced the WASM client-side authentication service with a new
SSR (Server-Side Rendering) authentication service to resolve
issues in SSR mode caused by self-referencing HTTP requests and
the lack of `HttpContext`.

Added `IEnvelopeAuthService` interface and its implementation
`EnvelopeAuthService`, which directly accesses `HttpContext.User`
to validate user authentication and claims. Registered the service
in the DI container with a scoped lifetime.

Updated `EnvelopeReceiverPage.razor` to use the new SSR service
for authentication checks and logout logic. Changes to the page
were reverted due to a merge conflict, with a detailed plan
provided for re-application.

Improved logging for debugging authentication issues and outlined
a migration checklist, including testing, unit tests, and
documentation updates. These changes improve performance, ensure
SSR compatibility, and eliminate unnecessary HTTP requests.
2026-06-24 16:08:29 +02:00
05f64e2b61 Refactor LoginReceiverPage for readability and visuals
Updated SVG paths for improved icon rendering and appearance.
Reformatted code for consistent style, including moving braces
to new lines and improving indentation. Enhanced error handling
logic for `LoginResult` states with better readability. Updated
password input field toggle logic and replaced SVG icons for
show/hide functionality. Refactored `SubmitAsync` and
`OnKeyDownAsync` methods for clarity. Cleaned up unnecessary
whitespace and ensured overall maintainability.
2026-06-24 15:59:21 +02:00
ed17852542 Add EnvelopeAuthService for SSR authentication
Introduced `EnvelopeAuthService` and `IEnvelopeAuthService` to handle server-side authentication for envelope receiver pages.

- Registered `IEnvelopeAuthService` as a scoped service in `Program.cs`.
- Implemented `EnvelopeAuthService` to validate user authentication and envelope key matching using `IHttpContextAccessor` and JWT claims.
- Added methods to retrieve the authenticated envelope key and current user (`ClaimsPrincipal`).
- Prioritized `NameIdentifier` claim for envelope key extraction, with fallback to `sub` claim.
- Documented the service and interface with XML comments for clarity.

This centralizes authentication logic, ensuring reusability and adherence to SSR best practices.
2026-06-24 15:57:06 +02:00
9947774ba8 Add YARP reverse proxy for auth request forwarding
Added the `Yarp.ReverseProxy` package and configured the app to use
YARP for forwarding specific authentication-related API requests
to an external service (`auth-hub`). Updated `Program.cs` to load
YARP configuration from a new `yarp.json` file and added middleware
to map unmatched requests to the reverse proxy.

Replaced old routes and clusters with new routes (`auth-login`,
`auth-envelope-receiver-login`) and a new cluster (`auth-hub`)
pointing to `https://localhost:9090`. Configured route
transformations for path and query parameter adjustments.

These changes improve modularity and scalability by enabling
dynamic reverse proxy configuration and external service
integration.
2026-06-24 15:55:56 +02:00
c6c1decd2a Refactor to use IHttpClientFactory and remove ApiOptions
Replaced direct injection of HttpClient with IHttpClientFactory
across the codebase to improve HTTP client management and align
with best practices. Removed dependency on ApiOptions and
IOptions<ApiOptions> in multiple services, simplifying constructors
and reducing configuration complexity.

Updated FontLoader to use IHttpClientFactory for font loading
with relative paths. Adjusted comments and documentation to
reflect these changes. Cleaned up unused using directives
related to ApiOptions.
2026-06-24 10:01:19 +02:00
0fdaa1a38d Refactor HttpClient usage and simplify configuration
Updated HttpClient setup to use named clients for API calls
and DevExpress components, improving resource management
and scalability. Scoped a default HttpClient for PdfViewer
requirements. Removed ApiOptions configuration binding for
simplification. Updated FontLoader to use IHttpClientFactory
to align with modern best practices.
2026-06-24 10:01:03 +02:00
5d66de9f32 Refactor HttpClient and HttpContextAccessor setup
Moved HttpContextAccessor registration into the configuration
of the named HttpClient ("EnvelopeGenerator.Server") to support
server-side rendering (SSR) scenarios. Updated the HttpClient
to dynamically set its BaseAddress based on the current request's
scheme and host using HttpContextAccessor. Removed standalone
HttpContextAccessor registration and updated related comments.
2026-06-24 10:00:43 +02:00
b6ec5307b6 Refactor HTTP client management and service lifetimes
Updated DependencyInjection.cs to change ISmsSender and
IEnvelopeSmsHandler lifetimes from Singleton to Scoped,
ensuring per-request instantiation. Added Microsoft.Extensions.Http
package to EnvelopeGenerator.Server.Client.csproj for enhanced
HttpClient handling. Refactored AnnotationService, AuthService,
DocumentService, EnvelopeReceiverService, SignatureCacheService,
and SignatureService to use IHttpClientFactory, improving
flexibility and testability. Introduced a named HttpClient
"EnvelopeGenerator.Server" in Program.cs for internal API calls,
and removed the previous HttpClient setup using HttpContextAccessor.
Added necessary using directives for System.Net.Http across
service files to support these changes.
2026-06-22 17:35:00 +02:00
106e62a912 Refactor namespaces to EnvelopeGenerator.Server
Renamed namespaces and related identifiers from EnvelopeGenerator.WebUI
to EnvelopeGenerator.Server across the project. This change affects
data models, services, controllers, and configuration files to ensure
consistency with the new architecture.

Updated @using directives in Razor components and other files to
reflect the new namespace structure. Adjusted project references in
EnvelopeGenerator.Server.csproj to point to the new
EnvelopeGenerator.Server.Client project.

Modified middleware and logging configurations to use the new
EnvelopeGenerator.Server namespace, including changes in Program.cs
and appsettings.json.

Updated resource and file references to use the new
EnvelopeGenerator.Server path, ensuring correct resource loading.

Adjusted configuration options in Program.cs to use the new namespace
for options classes, such as ApiOptions and PdfViewerOptions.

Updated authentication scheme names and related constants to align
with the new namespace structure.

Revised comments and documentation to reflect the new namespace,
ensuring clarity and consistency in the codebase.
2026-06-22 16:14:11 +02:00
27940f5d34 Refactor project structure in solution
Replaced "EnvelopeGenerator.WebUI" with "EnvelopeGenerator.Server" and "EnvelopeGenerator.WebUI.Client" with "EnvelopeGenerator.Server.Client". Updated project entries, solution configuration platforms, and nested projects to reflect these changes.
2026-06-22 15:17:34 +02:00
e776c2edb4 Update launchSettings.json with new profiles and URLs
Updated the `$schema` URL to use HTTPS. Modified `iisSettings` with new `applicationUrl` and `sslPort`. Removed old profiles (`http`, `https`, `IIS Express`) and added new ones: `https (Blazor UI)`, `https (Swagger API)`, `http (Development)`, and updated `IIS Express`. Removed `inspectUri` from `IIS Express` profile.
2026-06-22 15:06:46 +02:00
3f0f5d7fb9 Add Jenkins pipeline and update JSON comments
A new Jenkins pipeline has been added to the `Jenkinsfile` with a 'Build' stage executing `dotnet build`. The `appsettings.Development.json` file has been reformatted for consistency. In `appsettings.json`, comments have been added to explain the "Content-Security-Policy" nonce usage, logging levels, and the naming format for resource files in the `Cultures` section, aiding in localization management.
2026-06-22 14:57:53 +02:00
e11bc9df8e Add new controllers for envelope management
Introduced multiple controllers to enhance application functionality:
- `AnnotationController`: Manages annotations and signature lifecycle.
- `AuthController`: Handles user authentication and session management.
- `CacheController`: Manages cached data for receivers.
- `ConfigController`: Exposes client configuration data.
- `DocumentController`: Provides access to envelope documents.
- `EmailTemplateController`: Manages email templates.
- `EnvelopeController`: Manages envelope operations.
- `EnvelopeReceiverController`: Handles envelope receiver data.
- `EnvelopeTypeController`: Retrieves envelope types.
- `HistoryController`: Accesses envelope history.
- `IAuthController`: Defines authentication interface.
- `LocalizationController`: Manages localization settings.
- `ReadOnlyController`: Manages read-only envelope sharing.
- `ReceiverController`: Retrieves receiver data.
- `SignatureController`: Retrieves document signatures.
- `TfaRegistrationController`: Manages two-factor authentication.

These changes improve maintainability and scalability by organizing operations into dedicated controllers.
2026-06-22 14:57:26 +02:00
4dca17d39c Add claim extension methods for user authentication
Introduce `ReceiverClaimExtensions` and `SenderClaimExtensions` classes in the `EnvelopeGenerator.API.Extensions` namespace. These classes provide methods to extract specific claims from a `ClaimsPrincipal` object, aiding in user authentication.

In `ReceiverClaimExtensions.cs`, add methods to retrieve envelope-specific claims such as `EnvelopeUuid`, `ReceiverSignature`, `ReceiverMail`, `EnvelopeId`, and `ReceiverId`. Implement `GetRequiredClaimValue` to handle missing claims.

In `SenderClaimExtensions.cs`, add methods to extract sender-related claims like `GetId`, `GetUsername`, `GetName`, `GetPrename`, and `GetEmail`. Implement `GetRequiredClaimOfSender` for handling missing claims.

Both classes include XML documentation for clarity on method usage and exceptions.
2026-06-22 14:56:57 +02:00
8baf6b5553 Add AuthProxyDocumentFilter for Swagger customization
Introduce AuthProxyDocumentFilter to enhance OpenAPI docs by
adding custom operations for login and envelope receiver
login. Implement methods to define POST operations at
`/api/auth` and `/api/Auth/envelope-receiver/{key}` paths,
including request parameters and response descriptions.
Include necessary using directives for OpenAPI support.
2026-06-22 14:56:21 +02:00
3ca99fdd83 Add models for auth, contact, culture, and annotations
Introduce new classes and records in the `EnvelopeGenerator.API.Models` namespace to handle various functionalities:

- Add `Auth` record for managing authentication codes.
- Introduce `ContactLink` class for hyperlink management.
- Add `Culture` and `Cultures` classes for language and culture info.
- Implement `CustomImages` class for image management.
- Add `EnvelopeReceiverLogin` record for login requests.
- Introduce `ErrorViewModel` for error representation.
- Add `Image` class for image source and CSS management.
- Implement `Login` record for user authentication.
- Add `MainViewModel` with a nullable `Title` property.
- Introduce PDF annotation classes in `PsPdfKitAnnotation` namespace.
- Add `TFARegParams` class for 2FA registration parameters.
2026-06-22 14:56:03 +02:00
9e37bf1fe2 Enhance authentication and configuration setup
Introduced a new `AuthScheme` class for JWT authentication
schemes. Added `ExceptionHandlingMiddleware` for global
exception handling. Updated `Program.cs` to refactor service
registrations, including Blazor, API controllers, CORS, and
Swagger setup. Removed YARP reverse proxy and added a more
comprehensive configuration for authentication and caching.
Updated `appsettings.json` and `appsettings.Development.json`
with new sections for authentication, logging, and various
application-specific settings. Added new classes for handling
authentication tokens, connection strings, and cache options.
2026-06-22 14:28:43 +02:00
9a0837caa9 Refactor rendering and add PDF resource
Removed `@rendermode="InteractiveAuto"` from `<HeadOutlet />` and `<Routes />` in `App.razor` to adjust rendering mode. Updated `EnvelopeReceiverPage_DxPdfViewer.razor` to use `DevExpress.Blazor.PdfViewer` instead of `DevExpress.Blazor`. Added `@using DevExpress.Blazor` to `_Imports.razor` for project-wide access to DevExpress components. Embedded `Resources\Invoice.pdf` in `EnvelopeGenerator.WebUI.csproj` and added the PDF file to the project.
2026-06-22 10:44:34 +02:00
030646f33d Enhance service configuration and DI setup
Added `EnvelopeGenerator.WebUI.Client.Services` to the using directives. Registered `IHttpContextAccessor` to access HTTP context for request-specific information. Modified `HttpClient` setup to dynamically set the base address using the current request's host. Introduced several business services (`DocumentService`, `AuthService`, `AnnotationService`, `EnvelopeReceiverService`, `SignatureService`, `SignatureCacheService`, `AppVersionService`) to the service collection, indicating new features. Maintained existing YARP configuration. Noted the importance of DevExpress services for `DxPdfViewer`.
2026-06-18 16:15:00 +02:00
88317e40f5 Add AGENTS.md - Quick-start guide for AI agents
- Architecture overview (Blazor Auto Server+WASM hybrid)
- Critical development commands (both API and WebUI must run)
- Route structure with render mode requirements
- Coordinate system conversions (INCHES in DB)
- API architecture quirks and missing endpoints
- Status color coding from legacy VB.NET app
- Common mistakes to avoid
- Configuration locations and migration status
2026-06-18 13:33:21 +02:00
6fe99d0cd0 docs: add culture management migration warnings for Server/Auto
Prepare for Blazor Server/Auto migration by documenting current
WASM-specific culture initialization approach and providing
detailed migration checklist in COPILOT_CONTEXT.md.
2026-06-18 12:50:45 +02:00
45018d04b1 Refactor culture initialization logic
Moved culture initialization from App.razor to Program.cs to ensure culture settings are applied before the app starts. Removed CultureService injection and OnInitializedAsync method from App.razor. Updated LanguageSelector.razor to change language without page reload, enhancing user experience. Added System.Globalization to Program.cs for culture support.
2026-06-18 12:43:56 +02:00
b5af3e61ed Improve language display in LanguageSelector
Replaced the placeholder arrow in the language selector button with the current language name using the new `GetLanguageName` method. Added `GetLanguageName` to map culture codes to language names. Fixed encoding issue by correcting "Fran�ais" to "Français" in the French language option.
2026-06-18 10:34:05 +02:00
314608f27f Fix duplicate root route entry in ReceiverUI docs
Removed a redundant entry for the root route (`/`) in the
`ReceiverUI Route Structure` section of the documentation.
Both entries referenced the same file (`Index.razor`) and
served the same purpose, making the duplication unnecessary.
2026-06-17 17:04:52 +02:00
ba9f233993 Add Language Selector component to footer
Introduced a Language Selector component (`LanguageSelector.razor`) to enable users to switch between German, English, and French. The component includes a dropdown UI with flag icons and language labels, along with logic for toggling the dropdown and changing the app's culture.

Injected `IJSRuntime`, `NavigationManager`, and `CultureService` into the component to handle culture changes and navigation updates.

Updated `MainLayout.razor` to include the Language Selector in the footer. Refactored footer layout in `MainLayout.razor.css` to use flexbox for better alignment and spacing.

Added styles for the Language Selector in `app.css` to ensure a clean and responsive design. Integrated the `flag-icons` library in `index.html` to display flag icons for language representation.
2026-06-17 16:57:39 +02:00
9d962708c4 Initialize culture settings in App.razor
Added dependency injection for `CultureService` in `App.razor` to dynamically initialize culture settings during the component's lifecycle.

Included `System.Globalization` and `EnvelopeGenerator.ReceiverUI.Services` namespaces to support culture-related functionality.

Injected `CultureService` and added an `OnInitializedAsync` method to set `CultureInfo.DefaultThreadCurrentCulture` and `CultureInfo.DefaultThreadCurrentUICulture` based on the service's initialization.

Reformatted surrounding `<Router>` code for clarity.
2026-06-17 16:55:44 +02:00
c93a056ca5 Add CultureService and localization support
Added a new `CultureService` to manage application culture and
localization. The service supports retrieving and setting the
application culture, storing it in `localStorage`, and initializing
it based on stored values, browser settings, or a default fallback.

Registered `CultureService` in the dependency injection container
and added localization support in `Program.cs` using
`builder.Services.AddLocalization()`.
2026-06-17 16:54:40 +02:00
a88a26c248 Remove EGDbContextFactory and related configuration code
The `EGDbContextFactory` class, which implemented the
`IDesignTimeDbContextFactory<EGDbContext>` interface, has been
removed. This includes the `CreateDbContext` method, which handled
design-time DbContext creation by loading configuration from
`appsettings.migration.json`, setting up `DbContextOptions`, and
constructing `DbTriggerParams`.

Additionally, the `#if NET` preprocessor directive and its
corresponding `#endif` have been removed, along with all associated
`using` directives. This change suggests that the factory is no
longer needed due to a shift in the application's architecture or
design-time DbContext handling.
2026-06-17 16:26:24 +02:00
1e963ea215 Add support for JWT authentication
Added `Microsoft.AspNetCore.Authentication.JwtBearer` package to the project to enable JWT authentication. Updated `Program.cs` to include the necessary `using` directive for JWT authentication. No functional changes were made beyond the integration of the new package.
2026-06-17 16:25:57 +02:00
02b857382c Replace SignatureService with DocReceiverElementService
Refactor EnvelopeReceiverPage.razor to inject
DocReceiverElementService instead of SignatureService.

Update Program.cs to remove SignatureService from the
dependency injection configuration and add
DocReceiverElementService as a scoped service.

This change reflects a shift in architecture where
DocReceiverElementService now handles functionality
previously managed by SignatureService.
2026-06-17 16:09:57 +02:00
ca4ec7cb6f Rename SignatureController to DocReceiverElementController
The class `SignatureController` has been renamed to `DocReceiverElementController` to better reflect its purpose. The constructor and its XML documentation have been updated accordingly. The `[Authorize]`, `[ApiController]`, and `[Route]` attributes remain unchanged. A `TODO` comment has been added to indicate future updates for using a signature query.
2026-06-17 16:09:26 +02:00
f2356b3ce4 Refactor SignatureService to DocReceiverElementService
Renamed the `SignatureService` class to `DocReceiverElementService` to reflect its updated purpose. Updated the `GetAsync` method to use the `DocReceiverElement` API endpoint (`/api/DocReceiverElement/{envelopeKey}`) instead of the `Signature` API endpoint. These changes align the service with the new `DocReceiverElement` context.
2026-06-17 16:07:43 +02:00
d61fe79613 Add localization support to the application
Added a package reference for `Microsoft.Extensions.Localization`
to enable localization support. Registered localization services
in `Program.cs` using `builder.Services.AddLocalization()`.
Introduced the `Microsoft.Extensions.Localization` and
`EnvelopeGenerator.Application.Resources` namespaces to support
localized resources. These changes allow the application to
provide content in multiple languages or regions.
2026-06-17 16:05:26 +02:00
714cb9555f feat(i18n): add sender-side UI resources and extension methods
- Added 46 resource strings (de-DE, en-US, fr-FR) for sender UI
- Added corresponding extension methods in Resource.cs
- Migrated from Form app for ReceiverUI sender pages
2026-06-17 15:58:30 +02:00
315a022cb8 feat: Add sender-side UI resource strings for all languages
Added 46 new localized strings to support sender-side UI implementation:

Resources added:
- Resource.de-DE.resx (German)
- Resource.en-US.resx (English)
- Resource.fr-FR.resx (French)

Categories covered:
- Dashboard actions (NewEnvelope, LoadEnvelope, DeleteEnvelope, etc.)
- Grid columns (Receivers, Status, Type, CreatedOn, etc.)
- Toolbar actions (RefreshData, ShowDocument, ResendInvitation, etc.)
- Envelope editor (AddFile, MergeFiles, EditFields, SendEnvelope, etc.)
- Settings (Language, UseAccessCode, TwoFactorEnabled, etc.)
- Reminders & expiry (FirstReminderDays, ExpiresWhenDays, etc.)

Source: Migrated from EnvelopeGenerator.Form resx files
Purpose: Support upcoming ReceiverUI sender pages implementation
2026-06-17 15:55:13 +02:00
746635979b Ensure _allEnvelopes is never null after assignment
Modified the assignment of `_allEnvelopes` to use the result of
`EnvelopeService.GetAsync()` or an empty list (`[]`) if the
result is null. This change prevents potential null reference
issues when `_allEnvelopes` is used later in the code.
2026-06-17 15:10:31 +02:00
31548728cd Refactor EnvelopeStatus to shared domain library
Moved the `EnvelopeStatus` enum and its extension methods
from `ReceiverUI.Models` to `Domain.Constants` to improve
code reuse and maintainability. Updated `EnvelopeSenderPage.razor`
to reference the new namespace.
2026-06-17 15:10:17 +02:00
06c8af2ed8 Remove EnvelopeGenerator.Dto project from solution
The `EnvelopeGenerator.Dto.csproj` file has been removed entirely, including its `<Project>` XML structure.

References to `EnvelopeGenerator.Dto` have been removed from `EnvelopeGenerator.sln`, including the project declaration, build configurations in `SolutionConfigurationPlatforms`, and nested project mappings in `NestedProjects`.

This change fully removes the `EnvelopeGenerator.Dto` project from the solution.
2026-06-17 14:38:30 +02:00
9f57baf2e5 refactor(ReceiverUI/Models): update to use Application layer's DTO 2026-06-17 14:05:01 +02:00
73d793f0a0 remove ApiExplorerSettings attribute 2026-06-17 11:53:01 +02:00
65bb68feef Update DevExpress package versions to 25.2.8
Updated the following DevExpress package references in the project:
- `DevExpress.Blazor.PdfViewer` from 25.2.3 to 25.2.8
- `DevExpress.Blazor.Reporting.JSBasedControls` from 25.2.3 to 25.2.8
- `DevExpress.Blazor.Reporting.Viewer` from 25.2.3 to 25.2.8
- `DevExpress.Drawing.Skia` from 25.2.3 to 25.2.8

No changes were made to other package references or the `NativeFileReference` entry.
2026-06-17 11:05:53 +02:00
c5e97ee30b move dto to common dir 2026-06-17 09:46:27 +02:00
3a4f449b59 Add EnvelopeGenerator.Dto project to the solution
Introduce a new project, `EnvelopeGenerator.Dto`, targeting .NET 8.0.
The project is configured with implicit `using` directives and nullable
reference types enabled. Update the solution file to include the new
project, its build configurations (Debug/Release for Any CPU), and its
hierarchical relationship in the `NestedProjects` section.
2026-06-17 09:45:22 +02:00
6ca7767e4d Enhance grid functionality in EnvelopeSenderPage
Added support for column reordering, sorting, and resizing in the grid:
- Enabled `AllowColumnReorder` for column reordering.
- Enabled `AllowSort` to allow sorting of grid columns.
- Set `ColumnResizeMode` to `GridColumnResizeMode.ColumnsContainer` for improved column resizing behavior.
2026-06-17 09:41:46 +02:00
4237f0a815 Add CellDisplayTemplate for ID column in grid
Updated the `DxGridDataColumn` for the `Id` field in
`EnvelopeSenderPage.razor` to include a `CellDisplayTemplate`.
This enables customized rendering of the `Id` property from
the `EnvelopeDto` object, allowing for additional formatting
or logic during display.
2026-06-16 17:01:38 +02:00
3302be9348 Refactor grid columns and improve UI styling
Removed fixed column widths in `EnvelopeSenderPage.razor` for dynamic sizing. Added `CellDisplayTemplate` to `Title`, `Status`, and `EnvelopeReceivers` columns for custom data rendering. Fixed gradient typo in progress bar CSS.

Added a new CSS rule to hide empty DevExpress grid cells and updated `sender-page.css` to include this rule while preserving existing styles.
2026-06-16 16:47:11 +02:00
4572e20c51 Restrict Logout method to Sender auth scheme
The `[Authorize]` attribute on the `Logout` method in the
`AuthController` class was updated to use the
`AuthenticationSchemes = AuthScheme.Sender` instead of the
`Policy = AuthPolicy.SenderOrReceiver`. This change narrows
the authorization requirement, ensuring only users under the
`Sender` authentication scheme can access the `Logout`
functionality.
2026-06-16 16:32:53 +02:00
b3a70d7259 Add sender authentication check to EnvelopeSenderPage
Added an authentication check in `EnvelopeSenderPage.razor` to verify sender access before loading envelopes. Redirects unauthorized users to the sender login page.

Introduced `CheckSenderAsync` in `AuthService` to validate sender tokens via the `/api/auth/check` endpoint. Updated `OnInitializedAsync` to use this method, enhancing security by ensuring only authorized users can access envelope-related functionality.
2026-06-16 15:55:59 +02:00
bb81920d44 Refactor sender page styles and add versioned URLs
Moved inline styles from `EnvelopeSenderPage.razor` to a new
`sender-page.css` file for better maintainability and separation
of concerns. Updated `EnvelopeSenderPage.razor` to use versioned
URLs for stylesheets via the newly injected `AppVersionService`,
enabling cache-busting. Added responsive design support in
`sender-page.css` to improve layout on smaller screens.
2026-06-16 15:05:00 +02:00
3b66de0691 Enhance EnvelopeSenderPage with new UI and features
Integrated DevExpress Blazor components and added a responsive,
modern UI for the sender dashboard. Replaced placeholder content
with a functional layout, including a grid-based envelope viewer
with filtering, pagination, and detailed row templates.

Added status badges, progress indicators, and a sender action
bar with buttons for creating, editing, deleting, refreshing
envelopes, and logging out. Introduced loading and error
handling states for better user experience.

Refactored data loading with `LoadEnvelopesAsync` to fetch and
categorize envelopes. Added methods for envelope management
and logout functionality. Improved state management and removed
unused code. These changes lay the groundwork for future
enhancements.
2026-06-15 17:00:23 +02:00
9f6004ba8c Refactor EnvelopeDto and add EnvelopeStatus enum
Updated the `EnvelopeDto` class to use a simplified receiver model (`EnvelopeReceiverSimpleDto`) for streamlined data handling. Added the `EnvelopeReceiverSimpleDto` class to represent basic receiver information (`Name`, `Email`, `Signed`).

Introduced the `EnvelopeStatus` enumeration in `EnvelopeStatus.cs` to define envelope lifecycle statuses, repurposed for the `ReceiverUI` context. Added `EnvelopeStatusExtensions` with `IsActive` and `IsCompleted` methods to evaluate envelope status states.
2026-06-15 16:59:37 +02:00
ef246bae32 Add LogoutSenderAsync method to AuthService
A new asynchronous method `LogoutSenderAsync` was added to the `AuthService` class to handle sender user logout. The method sends a POST request to the `/api/auth/logout` endpoint and removes the authentication cookie. It accepts an optional `CancellationToken` parameter and returns a `bool` indicating the success of the operation. XML documentation comments were included to describe the method's functionality.
2026-06-15 16:59:16 +02:00
e4ebb29969 Add authorization and data fetching to sender page
Added an `[Authorize]` attribute with the "Sender" policy to restrict access to the `EnvelopeSenderPage.razor`. Updated the page title to "Umschläge" and added placeholder text for data loading.

Injected `EnvelopeService` and `IJSRuntime` to fetch and log active and completed envelopes. Introduced `_activeEnvelopes` and `_completedEnvelopes` fields to store fetched data. Configured `JsonSerializerOptions` for consistent JSON handling.

Implemented `OnInitializedAsync` to fetch data asynchronously, log results to the console, and handle errors gracefully.
2026-06-15 16:24:45 +02:00
83cdb9dfe9 Update launch settings for HTTPS and ReceiverUI profiles
Changed the `launchUrl` for the HTTPS profile in `launchSettings.json` from `"swagger"` to `"sender"`, updating the default URL path.
Disabled `launchBrowser` for the `EnvelopeGenerator.ReceiverUI` profile, preventing the browser from automatically opening when this profile is executed.
2026-06-15 16:20:28 +02:00
c5db676e01 Add EnvelopeService to DI container in Program.cs
Registered EnvelopeService with a scoped lifetime in the
dependency injection container by adding
`builder.Services.AddScoped<EnvelopeService>();` to Program.cs.
This ensures a new instance is created per HTTP request.
2026-06-15 15:56:49 +02:00
95c8e15887 Add EnvelopeDto and EnvelopeService for API integration
Introduced the `EnvelopeDto` class to represent envelope data with JSON property mappings. Added the `EnvelopeService` class to handle API interactions, including fetching envelopes with optional filters and query string construction using `Microsoft.AspNetCore.WebUtilities`. Updated the project file to include the required package reference for query string manipulation.
2026-06-15 15:40:59 +02:00
561b844e59 Add filtering for active and completed envelopes
Added `OnlyActive` and `OnlyCompleted` properties to the `ReadEnvelopeQuery` class to enable filtering envelopes by their active or completed status. Updated the `ReadEnvelopeQueryHandler` to apply these filters when the properties are set.

Enhanced the `EnvelopeStatus` class by introducing `Active` and `Completed` status lists and adding extension methods (`IsActive` and `IsCompleted`) to determine status categories. Included necessary `using` directives for `System` and `System.Linq`.
2026-06-15 15:07:12 +02:00
011960be75 Add EnvelopeStatus extensions and update documentation URL
The `System` namespace was added to `EnvelopeStatus.cs` to enable additional functionality. A documentation URL in the comments was updated to point to a new location, replacing the outdated link.

Introduced a new static class `EnvelopeStatusExtensions` with two extension methods for the `EnvelopeStatus` enum:
- `IsActive`: Checks if the status is active (between `EnvelopeCreated` and `EnvelopePartlySigned`).
- `IsCompleted`: Checks if the status is completed (between `EnvelopeCompletelySigned` and `EnvelopeWithdrawn`).
2026-06-15 14:55:01 +02:00
151c785af9 Enhance JSON options and authorization policies
Added JSON serialization options to ignore reference cycles in
the `AddControllers` method by configuring `ReferenceHandler`
to `IgnoreCycles`. Updated the `AddAuthorizationBuilder` to
include authentication schemes for the `SenderOrReceiver`
policy, requiring roles and schemes for enhanced security.
2026-06-12 15:21:50 +02:00
fa354a05cc Update authorization policy in ConfigController
Replaced the generic [Authorize] attribute with a more specific
[Authorize(Policy = AuthPolicy.SenderOrReceiver)] to enforce
a stricter authorization policy. Added a `using` directive for
`EnvelopeGenerator.Domain.Constants` to support the new policy.
2026-06-12 15:21:29 +02:00
1326407462 Update AuthController to use specific auth scheme
The `[Authorize]` attribute on the `Check` method was updated to specify the `AuthScheme.Sender` authentication scheme. This change ensures that the `Check` endpoint now requires authentication using this specific scheme, enhancing security and supporting multiple authentication schemes within the application.
2026-06-12 15:15:44 +02:00
a3c653ddb3 Simplify Envelope to EnvelopeDto mapping
Removed custom mapping logic for the `Receivers` property in the
`Envelope` to `EnvelopeDto` mapping within the `MappingProfile`
class. The mapping now uses default behavior without projecting
`EnvelopeReceivers` to `Receivers`.
2026-06-12 15:15:09 +02:00
8d736cdc5e Refactor EnvelopeDto property for receiver handling
Replaced the `Receivers` property with `EnvelopeReceivers` in the `EnvelopeDto` class to improve clarity and better align with the updated data model. The new property uses `IEnumerable<EnvelopeReceiverDto>?` instead of `IEnumerable<ReceiverDto>?`.
2026-06-12 15:14:51 +02:00
192 changed files with 6729 additions and 589 deletions

263
AGENTS.md Normal file
View File

@@ -0,0 +1,263 @@
# EnvelopeGenerator - Agent Guide
## Must Read First
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate
## Active Architecture (Post-Migration)
**Frontend:** Blazor Auto (Server+WASM hybrid)
- **WebUI** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
- **WebUI.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
**Backend:** EnvelopeGenerator.API (ASP.NET Core 8.0)
**Proxy:** YARP in WebUI routes `/api/*``localhost:8088` (API)
### Deprecated Projects - DO NOT USE
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to WebUI)
- `EnvelopeGenerator.Web` - Razor Pages (replaced by WebUI)
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
## Development Commands
### Run Both Projects (Required)
```powershell
# Terminal 1 - API Backend
cd EnvelopeGenerator.API
dotnet run
# Terminal 2 - Blazor Frontend
cd EnvelopeGenerator.WebUI\EnvelopeGenerator.WebUI
dotnet run
```
**Critical:** Both must run simultaneously. WebUI proxy forwards `/api/*` to API.
### Build
```powershell
dotnet build EnvelopeGenerator.sln
```
## Project Boundaries
```
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
EnvelopeGenerator.API/ # Controllers, endpoints
EnvelopeGenerator.WebUI/ # Server-side Blazor components
├─ Components/Pages/ # @rendermode InteractiveServer
EnvelopeGenerator.WebUI.Client/ # Client-side WASM components
├─ Pages/ # @rendermode InteractiveWebAssembly
├─ Services/ # HTTP API clients
├─ Models/ # DTOs
```
## Route Structure (Critical)
| Route | File Location | Render Mode | Purpose |
|-------|--------------|-------------|---------|
| `/` | `WebUI.Client/Pages/Index.razor` | WASM | Landing page |
| `/sender/login` | `WebUI.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
| `/sender` | `WebUI.Client/Pages/EnvelopeSenderPage.razor` | WASM | Sender dashboard |
| `/envelope/login/{key}` | `WebUI.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
| `/envelope/{key}` | `WebUI/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing |
**Rule:** PDF viewers MUST use `@rendermode InteractiveServer` (DevExpress backend requirement). Everything else uses WASM.
## Coordinate System (CRITICAL)
**Database stores INCHES** (GdPicture14 native). Origin: top-left, Y-axis down.
### Conversions
```csharp
// Database (INCHES) → PDF Points
float points = inches * 72;
// Database (INCHES) → DevExpress DX
float dx = inches * 100;
// PDF.js Pixels → Database (INCHES)
float inches = (pixelX / canvasWidth) * pageWidthInches;
```
**A4 Page:** 8.27" wide × 11.69" tall = 595pt × 842pt
**Signature Field Size:** 1.77" × 1.96" (FIXED, do not change)
**Evidence:** See `COPILOT_CONTEXT.md` lines 158-185, `EnvelopeGenerator.Form/frmFieldEditor.vb`
## API Architecture Quirks
### Monolithic Endpoint (Avoid for UI)
`POST /api/EnvelopeReceiver` - Creates envelope+document+receivers+fields atomically.
- **Use case:** External API consumers
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
### Missing Granular Endpoints (Need to Create)
```
POST /api/Envelope/draft # Create draft envelope
PUT /api/Envelope/{id} # Update metadata
DELETE /api/Envelope/{id} # Delete with reason
POST /api/Envelope/{id}/document # Upload PDF
POST /api/Envelope/{id}/receivers # Add receiver
POST /api/Envelope/{id}/signature-fields # Place signature field
POST /api/Envelope/{id}/send # Send to receivers
```
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
## Status Color Coding
Form app uses DevExpress `CustomDrawCell`. WebUI needs CSS:
```css
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
.envelope-row.status-queued,
.envelope-row.status-sent { background-color: #FFB74D; } /* ORANGE_300 */
.envelope-row.status-completed { background-color: #81C784; }
.envelope-row.status-deleted,
.envelope-row.status-rejected { background-color: #E57373; } /* RED_300 */
```
## Configuration
### YARP Proxy (`WebUI/yarp.json`)
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*``https://localhost:8088`
### PDF.js Settings (`WebUI/wwwroot/appsettings.json`)
```json
{
"PdfViewerOptions": {
"ThumbnailBaseScale": 0.75,
"ThumbnailEnableHiDPI": true,
"MainCanvasEnableHiDPI": true,
"ZoomStepPercentage": 5
}
}
```
### API Config (`API/appsettings.json`)
- `ConnectionStrings:Default` - SQL Server DB
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
- `PSPDFKitLicenseKey` - **DEPRECATED** (use PDF.js instead)
## Migration Status
### Complete ✅
- Receiver login/authentication
- PDF viewing with PDF.js (HiDPI, zoom, thumbnails)
- Signature capture (draw/type/image)
- Signature caching (Redis/SQL)
- Sender login
### Missing (High Priority) ❌
- Sender dashboard (`/sender`) - Empty stub
- Envelope editor (`/sender/envelope/{id}`)
- Signature field placement tool (PDF.js + draggable overlays)
- Granular API endpoints (draft, receivers, fields)
- Master-detail grids for receivers/history
## Common Mistakes (DO NOT REPEAT)
| Mistake | Why Wrong |
|---------|-----------|
| Using iText7 in receiver pages | GPL license issue. Use PDF.js overlays. |
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
| `@rendermode InteractiveWebAssembly` on PDF viewers | DevExpress DxPdfViewer requires server-side rendering. |
| Hardcoded quality in PDF.js | Use `appsettings.json` `PdfViewerOptions`. |
| Coordinates in points/pixels for DB | Database uses INCHES. Convert before save. |
| `BottomMarginBand` for signatures | Repeats on every page. Use `DetailBand`. |
## Testing
**No automated tests exist yet.**
Manual testing workflow:
1. Start API (`dotnet run` in `EnvelopeGenerator.API`)
2. Start WebUI (`dotnet run` in `EnvelopeGenerator.WebUI\EnvelopeGenerator.WebUI`)
3. Navigate to `https://localhost:5131` (or check console output for port)
4. Test sender login at `/sender/login`
5. Test receiver flow at `/envelope/login/{envelopeKey}`
## Database
**SQL Server** (DD_ECM)
- Connection string in `API/appsettings.json`
- EF Core migrations NOT used (manual SQL scripts)
- Stored procedures: `PRSIG_*` prefix
**Key Tables:**
- `TBSIG_ENVELOPE` - Envelope metadata
- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments
- `TBSIG_DOC_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
- `TBSIG_RECEIVER` - Receiver registry
- `TBSIG_DOCUMENT` - PDF binary data
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
## DevExpress
**License:** Commercial (v25.2.3)
**Components Used:**
- `DxGrid` - Master-detail grids
- `DxPdfViewer` - Server-side PDF rendering
- `DxPopup` - Modal dialogs
- `DxToolbar` - Action bars
- `DxFormLayout` - Forms
**Theme:** Blazing Berry (default)
## JavaScript Interop
**PDF Viewer:** `wwwroot/js/pdf-viewer.js`
```javascript
window.pdfViewer = {
initialize(canvasId, pdfDataUrl, dotNetRef),
renderPage(num),
renderSignatureButtons(signatures, pageNum, dotNetRef),
applySignature(signatureId, dataUrl, fullName, position, place),
zoomIn(), zoomOut(), dispose()
}
```
**Signature Pad:** `wwwroot/js/receiver-signature.js`
```javascript
window.receiverSignature = {
initializeDrawPad(canvasId, dotNetRef),
getSignatureDataUrl(canvasId),
clearPad(canvasId)
}
```
## Multi-Envelope Support
Receivers can login to **multiple envelopes simultaneously** via per-envelope cookies:
```
AuthTokenSignFLOWReceiver.{envelopeKey}
```
Each envelope maintains independent authentication state.
## External Dependencies
**CDN:**
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
**NuGet (WebUI.Client):**
- `DevExpress.Blazor.*` 25.2.3
- `SkiaSharp.*` 3.119.1 (WASM rendering)
**External Services:**
- LDAP/AD authentication (optional)
- GTX Messaging (SMS 2FA)
- Email dispatcher (signFlow)
## Environment Variables
None required. All config in `appsettings.json`.
**Local dev ports:**
- API: `https://localhost:8088`
- WebUI: `https://localhost:5131` or `http://localhost:7192`

View File

@@ -1,4 +1,4 @@
# EnvelopeGenerator — AI Context Reference
# EnvelopeGenerator — AI Context Reference
## Purpose
Digital document signing system with **unified Blazor Auto (Server+WASM hybrid) frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
@@ -125,6 +125,71 @@ Client ? WebUI:XXXX (Blazor Auto)
---
## Localization & Culture Management
**Current Architecture:** Blazor WebAssembly (client-side culture management)
### Implementation Details
**Culture Storage:**
- Culture preference stored in browser's `localStorage` (key: `AppCulture`)
- Managed by `CultureService.cs` (ReceiverUI/Services)
- Supported cultures: `de-DE`, `en-US`, `fr-FR`
**Culture Initialization:**
- **Location:** `Program.cs` (lines 53-57)
- Sets `CultureInfo.DefaultThreadCurrentCulture/UICulture` **before** app runs
- **WASM-Safe:** Each user has isolated browser instance
**Language Selector:**
- **Component:** `LanguageSelector.razor` (ReceiverUI/Shared)
- Displays flag icon + language name
- Changes culture via `CultureService.SetCultureAsync()`
- Navigates with `forceLoad: false` (smooth transition, no page reload)
### ⚠️ MIGRATION WARNING: Blazor Server/Auto
**Current approach is WASM-specific and will break in Server/Auto render modes!**
**Why it breaks:**
- `Program.cs:53-57` sets **global** `DefaultThreadCurrentCulture`
- In Server/Auto, one app instance serves **all users**
- User A selects German → User B sees German too (shared state)
- Thread-safety issues and culture conflicts
**Migration Checklist (when moving to Server/Auto):**
1. **Remove global culture initialization** from `Program.cs` (lines 53-57)
- See detailed warning comment in the code
2. **Add RequestLocalizationMiddleware** (Server-side approach):
```csharp
app.UseRequestLocalization(options => {
options.SupportedCultures = new[] { "de-DE", "en-US", "fr-FR" };
options.SupportedUICultures = options.SupportedCultures;
options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider());
});
```
3. **OR** Use **per-circuit culture** (Blazor Server approach):
- Store culture in circuit-scoped service
- Use `CascadingParameter` to distribute to components
- See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization
4. **Update `LanguageSelector.razor`:**
- Remove manual `CultureInfo.DefaultThreadCurrentCulture` assignment
- Use middleware/circuit culture provider instead
5. **Update `CultureService.cs`:**
- Integrate with Server-side culture provider
- May need to store in cookies instead of localStorage
**References:**
- Microsoft Docs: [Blazor Globalization/Localization](https://learn.microsoft.com/aspnet/core/blazor/globalization-localization)
- Current implementation: `Program.cs`, `CultureService.cs`, `LanguageSelector.razor`
---
## Key Files & Routes
### Client-Side Pages (WebUI.Client)
@@ -156,7 +221,7 @@ Client ? WebUI:XXXX (Blazor Auto)
---
## Coordinate System — CRITICAL
## Coordinate System — CRITICAL
**Database Format:** INCHES (GdPicture14 native)
**Origin:** Top-left corner
@@ -185,7 +250,7 @@ Client ? WebUI:XXXX (Blazor Auto)
---
## EnvelopeReceiver — PDF.js Viewer & Signing
## EnvelopeReceiver — PDF.js Viewer & Signing
**Route:** `/envelope/{EnvelopeKey}`
**Tech:** PDF.js 3.11.174 + Blazor Server (`@rendermode InteractiveServer`) + configurable quality
@@ -228,7 +293,7 @@ window.pdfViewer = {
---
## Signature Workflow — EnvelopeReceiver
## Signature Workflow — EnvelopeReceiver
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
@@ -281,9 +346,9 @@ public sealed record SignatureCaptureDto {
### API Endpoints
**Controller:** `API/Controllers/CacheController.cs`
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
**Cache Key Format:**
```
@@ -349,7 +414,7 @@ public async Task<SenderLoginResult> LoginSenderAsync(string username, string pa
**Response:**
- `200 OK` ? Cookie set, redirect to `/sender`
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
- Other ? Show error: "Serverfehler"
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
@@ -407,7 +472,7 @@ public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, st
---
## Mistakes History — Do NOT Repeat
## Mistakes History — Do NOT Repeat
| Mistake | Why Wrong |
|---|---|
@@ -426,9 +491,9 @@ public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, st
### Deprecated Projects
**DO NOT USE:**
- `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM) — Migrated to WebUI (DevExpress compatibility issue)
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified WebUI
- PSPDFKit — Removed, use PDF.js + DevExpress instead
- `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM) — Migrated to WebUI (DevExpress compatibility issue)
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified WebUI
- PSPDFKit — Removed, use PDF.js + DevExpress instead
### Legacy Projects (VB.NET)
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
@@ -453,8 +518,8 @@ Proves database uses INCHES natively.
## Quick Reference
### When working with coordinates:
1. **Database ? UI:** INCHES × 72 = PDF Points
2. **UI ? Display:** Points × scale = Pixels
1. **Database ? UI:** INCHES × 72 = PDF Points
2. **UI ? Display:** Points × scale = Pixels
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
### When adding features:

View File

@@ -40,7 +40,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
[HttpPost("logout")]
public async Task<IActionResult> Logout()
{
@@ -69,7 +69,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[HttpGet("check")]
[Authorize]
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
public IActionResult Check(string? role = null)
=> role is not null && !User.IsInRole(role)
? Unauthorized()

View File

@@ -1,4 +1,5 @@
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@@ -13,7 +14,7 @@ namespace EnvelopeGenerator.API.Controllers;
/// </remarks>
[Route("api/[controller]")]
[ApiController]
[Authorize]
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
{
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;

View File

@@ -0,0 +1,57 @@
using EnvelopeGenerator.API.Extensions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Documents.Queries;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
///
/// </summary>
[Authorize(Policy = AuthPolicy.Receiver)]
[ApiController]
[Route("api/[controller]")]
public class DocReceiverElementController : ControllerBase
{
private readonly IMediator _mediator;
/// <summary>
/// Initializes a new instance of <see cref="DocReceiverElementController"/>.
/// </summary>
public DocReceiverElementController(IMediator mediator)
{
_mediator = mediator;
}
//TODO: update to use signature query
/// <summary>
///
/// </summary>
/// <param name="envelopeKey"></param>
/// <param name="cancel"></param>
/// <returns></returns>
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpGet("{envelopeKey}")]
public async Task<IActionResult> Get(string envelopeKey, CancellationToken cancel)
{
int envelopeId = User.EnvelopeId();
int receiverId = User.ReceiverId();
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
if (doc.Elements is not IEnumerable<DocReceiverElementDto> docSignatures)
return NotFound("Document is empty.");
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();
if (rcvSignatures is null)
return NotFound("No signatures found for the current receiver.");
else
return Ok(rcvSignatures);
}
}

View File

@@ -34,6 +34,7 @@
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.28" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="itext" Version="8.0.5" />
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />

View File

@@ -13,7 +13,6 @@ using EnvelopeGenerator.Application;
using DigitalData.Auth.Client;
using DigitalData.Core.Abstractions;
using EnvelopeGenerator.API.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using DigitalData.Core.Abstractions.Security.Extensions;
using EnvelopeGenerator.API.Middleware;
@@ -22,6 +21,7 @@ using NLog.Web;
using NLog;
using DigitalData.Auth.Claims;
using EnvelopeGenerator.API;
using Microsoft.AspNetCore.Authentication.JwtBearer;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!");
@@ -44,7 +44,11 @@ try
var deferredProvider = new DeferredServiceProvider();
builder.Services.AddControllers();
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
});
builder.Services.AddHttpClient();
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
@@ -238,8 +242,9 @@ try
});
builder.Services.AddAuthorizationBuilder()
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy.RequireRole(Role.Sender, Role.Receiver.Full))
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy
.RequireRole(Role.Sender, Role.Receiver.Full)
.AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver))
.AddPolicy(AuthPolicy.Sender, policy => policy
.RequireRole(Role.Sender)
.AddAuthenticationSchemes(AuthScheme.Sender))

View File

@@ -22,8 +22,8 @@
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"launchBrowser": true,
"launchUrl": "sender",
"applicationUrl": "https://localhost:8088;http://localhost:5131",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Data Transfer Object representing configuration settings.
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class ConfigDto
{
/// <summary>

View File

@@ -1,14 +1,11 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Interfaces;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations.Schema;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Data Transfer Object representing a positioned element assigned to a document receiver.
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class DocReceiverElementDto : IDocReceiverElement
{
/// <summary>

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class DocumentDto
{
/// <summary>

View File

@@ -1,12 +1,10 @@
using EnvelopeGenerator.Domain.Constants;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Data Transfer Object representing the status of a document for a specific receiver.
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class DocumentStatusDto
{
/// <summary>

View File

@@ -1,19 +1,14 @@
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
using DigitalData.UserManager.Application.DTOs.User;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Interfaces;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeDto : IEnvelope
{
/// <summary>
@@ -133,5 +128,5 @@ public record EnvelopeDto : IEnvelope
/// <summary>
///
/// </summary>
public IEnumerable<ReceiverDto>? Receivers { get; set; }
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
}

View File

@@ -1,13 +1,11 @@
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeReceiverDto
{
/// <summary>

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
{
/// <summary>

View File

@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
///
/// </summary>
/// <param name="DateValid"></param>
[ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeReceiverReadOnlyCreateDto(
DateTime DateValid)
{

View File

@@ -1,6 +1,4 @@
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using Microsoft.AspNetCore.Mvc;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
@@ -8,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
/// Contains information about the receiver, associated envelope, and audit details.
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class EnvelopeReceiverReadOnlyDto
{
/// <summary>

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
/// <summary>
/// Data Transfer Object for updating a read-only envelope receiver.
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class EnvelopeReceiverReadOnlyUpdateDto
{
/// <summary>

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Data Transfer Object representing a type of envelope with its configuration settings.
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class EnvelopeTypeDto
{
/// <summary>

View File

@@ -1,9 +1,6 @@
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class GtxMessagingResponse : Dictionary<string, object?> { }

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record SmsResponse
{
/// <summary>

View File

@@ -1,5 +1,4 @@
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
@@ -7,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class ReceiverDto
{
/// <summary>

View File

@@ -7,7 +7,9 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common;
/// <summary>
/// Represents the AutoMapper profile configuration for mapping between
@@ -26,15 +28,15 @@ public class MappingProfile : Profile
CreateMap<DocReceiverElement, DocReceiverElementDto>();
CreateMap<DocumentStatus, DocumentStatusDto>();
CreateMap<EmailTemplate, EmailTemplateDto>();
CreateMap<Envelope, EnvelopeDto>().ForMember(dest => dest.Receivers, opt => opt.MapFrom(src => src.EnvelopeReceivers.Select(er => er.Receiver)));
CreateMap<Envelope, EnvelopeDto>();
CreateMap<Document, DocumentDto>();
CreateMap<Domain.Entities.History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
CreateMap<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
CreateMap<EnvelopeType, EnvelopeTypeDto>();
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
CreateMap<Receiver, ReceiverDto>();
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
CreateMap<ElementAnnotation, AnnotationDto>();
// DTO to Entity mappings
@@ -47,13 +49,13 @@ public class MappingProfile : Profile
CreateMap<EmailTemplateDto, EmailTemplate>();
CreateMap<EnvelopeDto, Envelope>();
CreateMap<DocumentDto, Document>();
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>();
CreateMap<EnvelopeTypeDto, EnvelopeType>();
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
CreateMap<AnnotationCreateDto, ElementAnnotation>()
.MapAddedWhen();

View File

@@ -51,8 +51,8 @@ public static class DependencyInjection
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
services.TryAddScoped<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); // Changed: Singleton → Scoped
services.TryAddSingleton<IAuthenticator, Authenticator>();
services.TryAddSingleton<QRCodeGenerator>();

View File

@@ -7,7 +7,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\Model.Designer.vb" />
</ItemGroup>
@@ -21,7 +21,6 @@
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="QRCoder" Version="1.6.0" />
@@ -79,25 +78,25 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="CommandDotNet">
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="CommandDotNet">
<Version>7.0.5</Version>
</PackageReference>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="CommandDotNet">
<Version>8.1.1</Version>
</PackageReference>
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="CommandDotNet">
<Version>8.1.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="CommandDotNet">
<Version>8.1.1</Version>
</PackageReference>
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="CommandDotNet">
<Version>8.1.1</Version>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -14,6 +14,16 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
/// </summary>
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
{
/// <summary>
///
/// </summary>
public bool OnlyActive { get; init; } = false;
/// <summary>
///
/// </summary>
public bool OnlyCompleted { get; init; } = false;
/// <summary>
/// Abfrage des Include des Umschlags
/// </summary>
@@ -132,6 +142,12 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
query = query.Where(e => !status.Ignore.Contains(e.Status));
}
if(request is { OnlyActive: true })
query = query.Where(e => Status.Active.Contains(e.Status));
if (request is { OnlyCompleted: true })
query = query.Where(e => Status.Completed.Contains(e.Status));
var envelopes = await query
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
.ToListAsync(cancel);

View File

@@ -3,7 +3,6 @@ using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
@@ -14,7 +13,6 @@ namespace EnvelopeGenerator.Application.Receivers.Commands;
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
{
/// <summary>

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Receivers.Commands;
namespace EnvelopeGenerator.Application.Receivers.Commands;
/// <summary>
/// Data Transfer Object for updating a receiver's information.
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class UpdateReceiverCommand
{
/// <summary>

View File

@@ -397,4 +397,412 @@ public static class Extensions
/// <param name="suffix"></param>
/// <returns></returns>
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
// Sender-side UI resources
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string NewEnvelope(this IStringLocalizer localizer) => localizer[nameof(NewEnvelope)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string LoadEnvelope(this IStringLocalizer localizer) => localizer[nameof(LoadEnvelope)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DeleteEnvelope(this IStringLocalizer localizer) => localizer[nameof(DeleteEnvelope)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string RefreshData(this IStringLocalizer localizer) => localizer[nameof(RefreshData)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string RefreshedAt(this IStringLocalizer localizer) => localizer[nameof(RefreshedAt)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ShowDocument(this IStringLocalizer localizer) => localizer[nameof(ShowDocument)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ContactReceiver(this IStringLocalizer localizer) => localizer[nameof(ContactReceiver)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string EnvelopeId(this IStringLocalizer localizer) => localizer[nameof(EnvelopeId)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string OpenLogDirectory(this IStringLocalizer localizer) => localizer[nameof(OpenLogDirectory)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ShowResultsReport(this IStringLocalizer localizer) => localizer[nameof(ShowResultsReport)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string SupportMail(this IStringLocalizer localizer) => localizer[nameof(SupportMail)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ResendInvitation(this IStringLocalizer localizer) => localizer[nameof(ResendInvitation)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Export(this IStringLocalizer localizer) => localizer[nameof(Export)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Receivers(this IStringLocalizer localizer) => localizer[nameof(Receivers)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string EmailSalutation(this IStringLocalizer localizer) => localizer[nameof(EmailSalutation)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string SignedWhen(this IStringLocalizer localizer) => localizer[nameof(SignedWhen)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string AccessCode(this IStringLocalizer localizer) => localizer[nameof(AccessCode)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string User(this IStringLocalizer localizer) => localizer[nameof(User)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Type(this IStringLocalizer localizer) => localizer[nameof(Type)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Title(this IStringLocalizer localizer) => localizer[nameof(Title)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string CreatedOn(this IStringLocalizer localizer) => localizer[nameof(CreatedOn)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string LastModified(this IStringLocalizer localizer) => localizer[nameof(LastModified)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string OpenEnvelopes(this IStringLocalizer localizer) => localizer[nameof(OpenEnvelopes)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string CompletedEnvelopes(this IStringLocalizer localizer) => localizer[nameof(CompletedEnvelopes)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string SendAccessCode(this IStringLocalizer localizer) => localizer[nameof(SendAccessCode)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string TwoFactorProperties(this IStringLocalizer localizer) => localizer[nameof(TwoFactorProperties)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Name(this IStringLocalizer localizer) => localizer[nameof(Name)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string PhoneNumber(this IStringLocalizer localizer) => localizer[nameof(PhoneNumber)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string AddReceiver(this IStringLocalizer localizer) => localizer[nameof(AddReceiver)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DeleteReceiver(this IStringLocalizer localizer) => localizer[nameof(DeleteReceiver)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string AddFile(this IStringLocalizer localizer) => localizer[nameof(AddFile)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string MergeFiles(this IStringLocalizer localizer) => localizer[nameof(MergeFiles)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DeleteFile(this IStringLocalizer localizer) => localizer[nameof(DeleteFile)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ShowFile(this IStringLocalizer localizer) => localizer[nameof(ShowFile)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string EditFields(this IStringLocalizer localizer) => localizer[nameof(EditFields)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string EditData(this IStringLocalizer localizer) => localizer[nameof(EditData)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Save(this IStringLocalizer localizer) => localizer[nameof(Save)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string SendEnvelope(this IStringLocalizer localizer) => localizer[nameof(SendEnvelope)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Cancel(this IStringLocalizer localizer) => localizer[nameof(Cancel)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string AddSignature(this IStringLocalizer localizer) => localizer[nameof(AddSignature)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DeleteSignature(this IStringLocalizer localizer) => localizer[nameof(DeleteSignature)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Language(this IStringLocalizer localizer) => localizer[nameof(Language)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string UseAccessCode(this IStringLocalizer localizer) => localizer[nameof(UseAccessCode)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string TwoFactorEnabled(this IStringLocalizer localizer) => localizer[nameof(TwoFactorEnabled)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string CertificationType(this IStringLocalizer localizer) => localizer[nameof(CertificationType)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string FinalEmailToCreator(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToCreator)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string FinalEmailToReceivers(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToReceivers)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string SendReminderEmails(this IStringLocalizer localizer) => localizer[nameof(SendReminderEmails)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string FirstReminderDays(this IStringLocalizer localizer) => localizer[nameof(FirstReminderDays)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ReminderIntervalDays(this IStringLocalizer localizer) => localizer[nameof(ReminderIntervalDays)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ExpiresWhenDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWhenDays)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ExpiresWarningDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWarningDays)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Message(this IStringLocalizer localizer) => localizer[nameof(Message)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string EnvelopeType(this IStringLocalizer localizer) => localizer[nameof(EnvelopeType)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string AllOptions(this IStringLocalizer localizer) => localizer[nameof(AllOptions)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DeleteReason(this IStringLocalizer localizer) => localizer[nameof(DeleteReason)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string PleaseProvideReason(this IStringLocalizer localizer) => localizer[nameof(PleaseProvideReason)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string Status(this IStringLocalizer localizer) => localizer[nameof(Status)].Value;
}

View File

@@ -477,4 +477,178 @@
<data name="Confirmations" xml:space="preserve">
<value>Bestätigungen</value>
</data>
<data name="NewEnvelope" xml:space="preserve">
<value>Neuer Umschlag</value>
</data>
<data name="LoadEnvelope" xml:space="preserve">
<value>Umschlag laden</value>
</data>
<data name="DeleteEnvelope" xml:space="preserve">
<value>Umschlag zurückrufen/löschen</value>
</data>
<data name="RefreshData" xml:space="preserve">
<value>Daten Aktualisieren</value>
</data>
<data name="RefreshedAt" xml:space="preserve">
<value>Aktualisiert: {0}</value>
</data>
<data name="ShowDocument" xml:space="preserve">
<value>Dokument anzeigen</value>
</data>
<data name="ContactReceiver" xml:space="preserve">
<value>Empfänger kontaktieren</value>
</data>
<data name="EnvelopeId" xml:space="preserve">
<value>Umschlag-ID: {0}</value>
</data>
<data name="OpenLogDirectory" xml:space="preserve">
<value>Öffne Log Verzeichnis</value>
</data>
<data name="ShowResultsReport" xml:space="preserve">
<value>Ergebnisbericht anzeigen</value>
</data>
<data name="SupportMail" xml:space="preserve">
<value>Support Mail</value>
</data>
<data name="ResendInvitation" xml:space="preserve">
<value>Einladung manuell versenden</value>
</data>
<data name="Export" xml:space="preserve">
<value>Export</value>
</data>
<data name="Receivers" xml:space="preserve">
<value>Empfänger</value>
</data>
<data name="EmailSalutation" xml:space="preserve">
<value>Email Anrede</value>
</data>
<data name="SignedWhen" xml:space="preserve">
<value>Unterschrieben wann</value>
</data>
<data name="AccessCode" xml:space="preserve">
<value>Zugangscode</value>
</data>
<data name="User" xml:space="preserve">
<value>Benutzer</value>
</data>
<data name="Type" xml:space="preserve">
<value>Typ</value>
</data>
<data name="Title" xml:space="preserve">
<value>Titel</value>
</data>
<data name="CreatedOn" xml:space="preserve">
<value>Erstellt am</value>
</data>
<data name="LastModified" xml:space="preserve">
<value>Zuletzt geändert am</value>
</data>
<data name="OpenEnvelopes" xml:space="preserve">
<value>Offene Umschläge</value>
</data>
<data name="CompletedEnvelopes" xml:space="preserve">
<value>Abgeschlossene Umschläge</value>
</data>
<data name="SendAccessCode" xml:space="preserve">
<value>Zugangscode senden</value>
</data>
<data name="TwoFactorProperties" xml:space="preserve">
<value>2-Faktor Eigenschaften</value>
</data>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="PhoneNumber" xml:space="preserve">
<value>Telefonnummer</value>
</data>
<data name="AddReceiver" xml:space="preserve">
<value>Empfänger hinzufügen</value>
</data>
<data name="DeleteReceiver" xml:space="preserve">
<value>Empfänger löschen</value>
</data>
<data name="AddFile" xml:space="preserve">
<value>Datei hinzufügen</value>
</data>
<data name="MergeFiles" xml:space="preserve">
<value>Dateien zusammenführen</value>
</data>
<data name="DeleteFile" xml:space="preserve">
<value>Datei löschen</value>
</data>
<data name="ShowFile" xml:space="preserve">
<value>Datei anzeigen</value>
</data>
<data name="EditFields" xml:space="preserve">
<value>Felder bearbeiten</value>
</data>
<data name="EditData" xml:space="preserve">
<value>Daten bearbeiten</value>
</data>
<data name="Save" xml:space="preserve">
<value>Speichern</value>
</data>
<data name="SendEnvelope" xml:space="preserve">
<value>Umschlag versenden</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Abbrechen</value>
</data>
<data name="AddSignature" xml:space="preserve">
<value>Signatur hinzufügen</value>
</data>
<data name="DeleteSignature" xml:space="preserve">
<value>Signatur löschen</value>
</data>
<data name="Language" xml:space="preserve">
<value>Sprache</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Zugangscode verwenden</value>
</data>
<data name="TwoFactorEnabled" xml:space="preserve">
<value>2-Faktor-Authentifizierung aktiviert</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Zertifizierungstyp</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="ExpiresWarningDays" xml:space="preserve">
<value>Ablaufwarnung (Tage)</value>
</data>
<data name="Message" xml:space="preserve">
<value>Nachricht</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Umschlagtyp</value>
</data>
<data name="AllOptions" xml:space="preserve">
<value>Alle Optionen</value>
</data>
<data name="DeleteReason" xml:space="preserve">
<value>Grund für Löschung</value>
</data>
<data name="PleaseProvideReason" xml:space="preserve">
<value>Bitte geben Sie einen Grund an</value>
</data>
<data name="Status" xml:space="preserve">
<value>Status</value>
</data>
</root>

View File

@@ -477,4 +477,178 @@
<data name="Confirmations" xml:space="preserve">
<value>Confirmations</value>
</data>
<data name="NewEnvelope" xml:space="preserve">
<value>New Envelope</value>
</data>
<data name="LoadEnvelope" xml:space="preserve">
<value>Load Envelope</value>
</data>
<data name="DeleteEnvelope" xml:space="preserve">
<value>Delete Envelope</value>
</data>
<data name="RefreshData" xml:space="preserve">
<value>Reload Data</value>
</data>
<data name="RefreshedAt" xml:space="preserve">
<value>Refreshed: {0}</value>
</data>
<data name="ShowDocument" xml:space="preserve">
<value>Show Document</value>
</data>
<data name="ContactReceiver" xml:space="preserve">
<value>Contact Receiver</value>
</data>
<data name="EnvelopeId" xml:space="preserve">
<value>Envelope-ID: {0}</value>
</data>
<data name="OpenLogDirectory" xml:space="preserve">
<value>Open Log Directory</value>
</data>
<data name="ShowResultsReport" xml:space="preserve">
<value>Show Results Report</value>
</data>
<data name="SupportMail" xml:space="preserve">
<value>Support Mail</value>
</data>
<data name="ResendInvitation" xml:space="preserve">
<value>Send Invitation Again</value>
</data>
<data name="Export" xml:space="preserve">
<value>Export</value>
</data>
<data name="Receivers" xml:space="preserve">
<value>Receivers</value>
</data>
<data name="EmailSalutation" xml:space="preserve">
<value>Email Salutation</value>
</data>
<data name="SignedWhen" xml:space="preserve">
<value>Signed When</value>
</data>
<data name="AccessCode" xml:space="preserve">
<value>Access Code</value>
</data>
<data name="User" xml:space="preserve">
<value>User</value>
</data>
<data name="Type" xml:space="preserve">
<value>Type</value>
</data>
<data name="Title" xml:space="preserve">
<value>Title</value>
</data>
<data name="CreatedOn" xml:space="preserve">
<value>Created On</value>
</data>
<data name="LastModified" xml:space="preserve">
<value>Last Modified</value>
</data>
<data name="OpenEnvelopes" xml:space="preserve">
<value>Open Envelopes</value>
</data>
<data name="CompletedEnvelopes" xml:space="preserve">
<value>Completed Envelopes</value>
</data>
<data name="SendAccessCode" xml:space="preserve">
<value>Send Access Code</value>
</data>
<data name="TwoFactorProperties" xml:space="preserve">
<value>2-Factor Properties</value>
</data>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="PhoneNumber" xml:space="preserve">
<value>Phone Number</value>
</data>
<data name="AddReceiver" xml:space="preserve">
<value>Add Receiver</value>
</data>
<data name="DeleteReceiver" xml:space="preserve">
<value>Delete Receiver</value>
</data>
<data name="AddFile" xml:space="preserve">
<value>Add File</value>
</data>
<data name="MergeFiles" xml:space="preserve">
<value>Merge Files</value>
</data>
<data name="DeleteFile" xml:space="preserve">
<value>Delete File</value>
</data>
<data name="ShowFile" xml:space="preserve">
<value>Show File</value>
</data>
<data name="EditFields" xml:space="preserve">
<value>Edit Fields</value>
</data>
<data name="EditData" xml:space="preserve">
<value>Edit Data</value>
</data>
<data name="Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="SendEnvelope" xml:space="preserve">
<value>Send Envelope</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="AddSignature" xml:space="preserve">
<value>Add Signature</value>
</data>
<data name="DeleteSignature" xml:space="preserve">
<value>Delete Signature</value>
</data>
<data name="Language" xml:space="preserve">
<value>Language</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Use Access Code</value>
</data>
<data name="TwoFactorEnabled" xml:space="preserve">
<value>2-Factor Authentication Enabled</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Certification Type</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="ExpiresWarningDays" xml:space="preserve">
<value>Expiry Warning (Days)</value>
</data>
<data name="Message" xml:space="preserve">
<value>Message</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Envelope Type</value>
</data>
<data name="AllOptions" xml:space="preserve">
<value>All Options</value>
</data>
<data name="DeleteReason" xml:space="preserve">
<value>Deletion Reason</value>
</data>
<data name="PleaseProvideReason" xml:space="preserve">
<value>Please provide a reason</value>
</data>
<data name="Status" xml:space="preserve">
<value>Status</value>
</data>
</root>

View File

@@ -477,4 +477,178 @@
<data name="Confirmations" xml:space="preserve">
<value>Confirmations</value>
</data>
<data name="NewEnvelope" xml:space="preserve">
<value>Nouvelle enveloppe</value>
</data>
<data name="LoadEnvelope" xml:space="preserve">
<value>Charger l'enveloppe</value>
</data>
<data name="DeleteEnvelope" xml:space="preserve">
<value>Supprimer l'enveloppe</value>
</data>
<data name="RefreshData" xml:space="preserve">
<value>Actualiser les données</value>
</data>
<data name="RefreshedAt" xml:space="preserve">
<value>Actualisé : {0}</value>
</data>
<data name="ShowDocument" xml:space="preserve">
<value>Afficher le document</value>
</data>
<data name="ContactReceiver" xml:space="preserve">
<value>Contacter le destinataire</value>
</data>
<data name="EnvelopeId" xml:space="preserve">
<value>ID d'enveloppe : {0}</value>
</data>
<data name="OpenLogDirectory" xml:space="preserve">
<value>Ouvrir le répertoire des logs</value>
</data>
<data name="ShowResultsReport" xml:space="preserve">
<value>Afficher le rapport de résultats</value>
</data>
<data name="SupportMail" xml:space="preserve">
<value>E-mail de support</value>
</data>
<data name="ResendInvitation" xml:space="preserve">
<value>Renvoyer l'invitation</value>
</data>
<data name="Export" xml:space="preserve">
<value>Exporter</value>
</data>
<data name="Receivers" xml:space="preserve">
<value>Destinataires</value>
</data>
<data name="EmailSalutation" xml:space="preserve">
<value>Formule de politesse</value>
</data>
<data name="SignedWhen" xml:space="preserve">
<value>Signé quand</value>
</data>
<data name="AccessCode" xml:space="preserve">
<value>Code d'accès</value>
</data>
<data name="User" xml:space="preserve">
<value>Utilisateur</value>
</data>
<data name="Type" xml:space="preserve">
<value>Type</value>
</data>
<data name="Title" xml:space="preserve">
<value>Titre</value>
</data>
<data name="CreatedOn" xml:space="preserve">
<value>Créé le</value>
</data>
<data name="LastModified" xml:space="preserve">
<value>Dernière modification</value>
</data>
<data name="OpenEnvelopes" xml:space="preserve">
<value>Enveloppes ouvertes</value>
</data>
<data name="CompletedEnvelopes" xml:space="preserve">
<value>Enveloppes terminées</value>
</data>
<data name="SendAccessCode" xml:space="preserve">
<value>Envoyer le code d'accès</value>
</data>
<data name="TwoFactorProperties" xml:space="preserve">
<value>Propriétés 2-facteurs</value>
</data>
<data name="Name" xml:space="preserve">
<value>Nom</value>
</data>
<data name="PhoneNumber" xml:space="preserve">
<value>Numéro de téléphone</value>
</data>
<data name="AddReceiver" xml:space="preserve">
<value>Ajouter un destinataire</value>
</data>
<data name="DeleteReceiver" xml:space="preserve">
<value>Supprimer le destinataire</value>
</data>
<data name="AddFile" xml:space="preserve">
<value>Ajouter un fichier</value>
</data>
<data name="MergeFiles" xml:space="preserve">
<value>Fusionner les fichiers</value>
</data>
<data name="DeleteFile" xml:space="preserve">
<value>Supprimer le fichier</value>
</data>
<data name="ShowFile" xml:space="preserve">
<value>Afficher le fichier</value>
</data>
<data name="EditFields" xml:space="preserve">
<value>Modifier les champs</value>
</data>
<data name="EditData" xml:space="preserve">
<value>Modifier les données</value>
</data>
<data name="Save" xml:space="preserve">
<value>Enregistrer</value>
</data>
<data name="SendEnvelope" xml:space="preserve">
<value>Envoyer l'enveloppe</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Annuler</value>
</data>
<data name="AddSignature" xml:space="preserve">
<value>Ajouter une signature</value>
</data>
<data name="DeleteSignature" xml:space="preserve">
<value>Supprimer la signature</value>
</data>
<data name="Language" xml:space="preserve">
<value>Langue</value>
</data>
<data name="UseAccessCode" xml:space="preserve">
<value>Utiliser un code d'accès</value>
</data>
<data name="TwoFactorEnabled" xml:space="preserve">
<value>Authentification à 2 facteurs activée</value>
</data>
<data name="CertificationType" xml:space="preserve">
<value>Type de certification</value>
</data>
<data name="FinalEmailToCreator" xml:space="preserve">
<value>E-mail final au créateur</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 après (jours)</value>
</data>
<data name="ExpiresWarningDays" xml:space="preserve">
<value>Avertissement d'expiration (jours)</value>
</data>
<data name="Message" xml:space="preserve">
<value>Message</value>
</data>
<data name="EnvelopeType" xml:space="preserve">
<value>Type d'enveloppe</value>
</data>
<data name="AllOptions" xml:space="preserve">
<value>Toutes les options</value>
</data>
<data name="DeleteReason" xml:space="preserve">
<value>Motif de suppression</value>
</data>
<data name="PleaseProvideReason" xml:space="preserve">
<value>Veuillez indiquer une raison</value>
</data>
<data name="Status" xml:space="preserve">
<value>Statut</value>
</data>
</root>

View File

@@ -1,8 +1,10 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
namespace EnvelopeGenerator.Domain.Constants
{
// http://wiki.dd/xwiki13/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/SignFlow/Envelope%20Status/
// http://wiki.dd/xwiki_prod/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/signFLOW/signFLOW%20-%20Enwickler-Handbuch/4.%20Anhang/4.3%20Historie%20und%20Status%20der%20Umschl%C3%A4ge/
public enum EnvelopeStatus
{
Invalid = 0,
@@ -49,5 +51,28 @@ namespace EnvelopeGenerator.Domain.Constants
EnvelopeStatus.EnvelopeCreated,
EnvelopeStatus.DocumentMod_Rotation
};
public static readonly List<EnvelopeStatus> Active = Enum.GetValues(typeof(EnvelopeStatus))
.Cast<EnvelopeStatus>()
.Where(status => status.IsActive())
.ToList();
public static readonly List<EnvelopeStatus> Completed = Enum.GetValues(typeof(EnvelopeStatus))
.Cast<EnvelopeStatus>()
.Where(status => status.IsCompleted())
.ToList();
}
public static class EnvelopeStatusExtensions
{
public static bool IsActive(this EnvelopeStatus status)
{
return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned;
}
public static bool IsCompleted(this EnvelopeStatus status)
{
return status >= EnvelopeStatus.EnvelopeCompletelySigned && status <= EnvelopeStatus.EnvelopeWithdrawn;
}
}
}

View File

@@ -1,48 +0,0 @@
#if NET
using EnvelopeGenerator.Application.Common.Configurations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Infrastructure
{
public class EGDbContextFactory : IDesignTimeDbContextFactory<EGDbContext>
{
public EGDbContext CreateDbContext(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.migration.json")
.Build();
// create DbContextOptions
var optionsBuilder = new DbContextOptionsBuilder<EGDbContext>();
optionsBuilder.UseSqlServer(config.GetConnectionString("Default"));
// create DbTriggerParams
var triggerLists = config.GetSection("DbTriggerParams").Get<Dictionary<string, List<string>>>();
var dbTriggerParams = new DbTriggerParams();
if (triggerLists is not null)
foreach (var triggerList in triggerLists)
{
if (triggerList.Value.Count == 0)
continue; // Skip empty trigger lists
var tableName = triggerList.Key;
dbTriggerParams[tableName] = new List<string>();
foreach (var trigger in triggerList.Value)
{
dbTriggerParams[tableName].Add(trigger);
}
}
var dbContext = new EGDbContext(optionsBuilder.Options, Options.Create(dbTriggerParams));
dbContext.IsMigration = true;
return dbContext;
}
}
}
#endif

View File

@@ -1,4 +1,6 @@
<Router AppAssembly="@typeof(Program).Assembly">
@using System.Globalization
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
@@ -7,4 +9,4 @@
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</Router>

View File

@@ -24,11 +24,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.8" />
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.8" />
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.8" />
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.8" />
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.28" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.11" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
@@ -40,6 +42,9 @@
<ItemGroup>
<Folder Include="Properties\PublishProfiles\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\docs\privacy-policy.en-US.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>

View File

@@ -1,32 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Models;
/// <summary>
/// Represents a pre-assigned signature annotation position on a specific page.
/// <br/><br/>
/// <b>Coordinate unit (X, Y):</b> Inches (GdPicture14 native unit),
/// origin at the <b>top-left</b> corner of the page, both axes increase downward/rightward.
/// <br/><br/>
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
/// Convert: <c>xDX = xInches * 100.0</c>
/// <br/>
/// <b>Conversion to PDF Points:</b> Multiply by 72 (1 inch = 72 points).
/// Convert: <c>xPt = xInches * 72.0</c>
/// <br/>
/// <b>Y-axis for PDF (bottom-left origin):</b> Flip required for iText7.
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
/// </summary>
[Obsolete("Use SignatureDto with SignatureService.")]
public record AnnotationDto
{
/// <summary>Unique identifier of the annotation.</summary>
public long Id { get; init; }
/// <summary>1-based page number within the document.</summary>
public int Page { get; init; }
/// <summary>Horizontal position in INCHES from the left edge of the page.</summary>
public double X { get; init; }
/// <summary>Vertical position in INCHES from the top edge of the page.</summary>
public double Y { get; init; }
}

View File

@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.ReceiverUI.Models;
public class EnvelopeDto
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("uuid")]
public string? Uuid { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("status")]
public int Status { get; set; }
[JsonPropertyName("docResult")]
public byte[]? DocResult { get; set; }
[JsonPropertyName("envelopeReceivers")]
public List<EnvelopeReceiverSimpleDto> EnvelopeReceivers { get; set; } = new();
}
/// <summary>
/// Simplified receiver model for envelope list display
/// </summary>
public class EnvelopeReceiverSimpleDto
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("email")]
public string? Email { get; set; }
[JsonPropertyName("signed")]
public bool Signed { get; set; }
}

View File

@@ -11,7 +11,7 @@
@inject IOptions<ApiOptions> AppOptions
@inject IOptions<PdfViewerOptions> PdfViewerOptions
@inject IJSRuntime JSRuntime
@inject SignatureService SignatureService
@inject DocReceiverElementService SignatureService
@inject SignatureCacheService SignatureCacheService
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService

View File

@@ -1,7 +1,442 @@
@page "/sender"
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "Sender")]
<h3>EnvelopeSender</h3>
@using System.Text.Json
@using EnvelopeGenerator.Domain.Constants
@using EnvelopeGenerator.ReceiverUI.Models
@using DevExpress.Blazor
@using EnvelopeGenerator.ReceiverUI.Services
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeService EnvelopeService
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime
@inject AppVersionService AppVersion
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
<link href="@AppVersion.GetVersionedUrl("css/sender-page.css")" rel="stylesheet" />
<div class="sender-dashboard-layout">
<div class="sender-action-bar">
<div class="sender-action-bar__inner">
<div class="sender-title-section">
<div class="sender-logo">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
</svg>
</div>
<div class="sender-title">Umschlag-Übersicht</div>
</div>
<div class="sender-toolbar">
<button class="sender-btn sender-btn--primary" @onclick="CreateEnvelope" title="Neuen Umschlag erstellen">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
Neuer Umschlag
</button>
<button class="sender-btn" @onclick="EditEnvelope" disabled="@(_selectedEnvelope == null || IsEnvelopeSent(_selectedEnvelope))" title="Ausgewählten Umschlag bearbeiten">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
Bearbeiten
</button>
<button class="sender-btn sender-btn--danger" @onclick="DeleteEnvelope" disabled="@(_selectedEnvelope == null)" title="Ausgewählten Umschlag löschen">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>
Löschen
</button>
<button class="sender-btn" @onclick="RefreshEnvelopes" disabled="@_isLoading" title="Aktualisieren">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
@if (_isLoading) {
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
}
</button>
<button class="sender-btn sender-btn--logout" @onclick="LogoutAsync" disabled="@_isLoggingOut" title="Abmelden">
@if (_isLoggingOut) {
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
} else {
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
</svg>
}
</button>
</div>
</div>
</div>
<div class="sender-content">
@if (_isLoading && _allEnvelopes == null) {
<div class="d-flex justify-content-center align-items-center h-100">
<div class="text-center">
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
<span class="visually-hidden">Lädt...</span>
</div>
<p class="text-white fw-semibold">Umschläge werden geladen...</p>
</div>
</div>
} else if (_errorMessage != null) {
<div class="error-container">
<div class="alert alert-danger shadow-lg">
<div class="d-flex align-items-start">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" 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>
<h5 class="mb-2">Fehler beim Laden der Umschläge</h5>
<p class="mb-0">@_errorMessage</p>
</div>
</div>
</div>
</div>
} else {
<div class="sender-grid-container">
<div class="sender-tabs">
<button class="sender-tab @(_activeTab == "active" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "active"'>
<span>Aktive Umschläge</span>
@if (_activeEnvelopes != null) {
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_activeEnvelopes.Count())</span>
}
</button>
<button class="sender-tab @(_activeTab == "completed" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "completed"'>
<span>Abgeschlossene Umschläge</span>
@if (_completedEnvelopes != null) {
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_completedEnvelopes.Count())</span>
}
</button>
</div>
<div class="sender-grid-wrapper">
@if (_activeTab == "active") {
<DxGrid Data="@_activeEnvelopes"
@ref="_gridActive"
ShowFilterRow="true"
ShowSearchBox="true"
AllowColumnReorder="true"
AllowSort=true
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
PageSize="20"
PagerVisible="true"
SelectionMode="GridSelectionMode.Single"
SelectedDataItem="@_selectedEnvelope"
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
CustomizeElement="OnCustomizeElement">
<Columns>
<DxGridDataColumn FieldName="Id" Caption="ID">
<CellDisplayTemplate Context="cellContext">
@((cellContext.DataItem as EnvelopeDto)?.Id)
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Title" Caption="Titel">
<CellDisplayTemplate Context="cellContext">
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Status" Caption="Status">
<CellDisplayTemplate Context="cellContext">
@{
var envelope = cellContext.DataItem as EnvelopeDto;
if (envelope != null) {
var statusInfo = GetStatusInfo(envelope.Status);
<div class="status-badge status-badge--@statusInfo.CssClass">
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
@statusInfo.Label
</div>
}
}
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger">
<CellDisplayTemplate Context="cellContext">
@{
var envelope = cellContext.DataItem as EnvelopeDto;
if (envelope != null) {
var receivers = envelope.EnvelopeReceivers ?? new List<EnvelopeReceiverSimpleDto>();
var signed = receivers.Count(r => r.Signed);
var total = receivers.Count;
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 0.875rem; color: #6b7280;">
@signed / @total unterschrieben
</span>
@if (total > 0) {
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
</div>
}
</div>
}
}
</CellDisplayTemplate>
</DxGridDataColumn>
</Columns>
<DetailRowTemplate Context="detailContext">
<div style="padding: 1rem; background: #f9fafb;">
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
@{
var envelope = detailContext.DataItem as EnvelopeDto;
if (envelope?.EnvelopeReceivers?.Any() == true) {
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
@foreach (var receiver in envelope.EnvelopeReceivers) {
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
<span class="receiver-badge receiver-badge--@(receiver.Signed ? "signed" : "unsigned")" style="min-width: 100px;">
@if (receiver.Signed) {
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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>
<span>Unterschrieben</span>
} else {
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>
<span>Ausstehend</span>
}
</span>
<div style="flex: 1; font-size: 0.875rem;">
<strong style="color: #1f2937;">@receiver.Name</strong>
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Email</span>
</div>
</div>
}
</div>
} else {
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
}
}
</div>
</DetailRowTemplate>
</DxGrid>
} else {
<DxGrid Data="@_completedEnvelopes"
@ref="_gridCompleted"
ShowFilterRow="true"
ShowSearchBox="true"
PageSize="20"
PagerVisible="true"
SelectionMode="GridSelectionMode.Single"
SelectedDataItem="@_selectedEnvelope"
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
CustomizeElement="OnCustomizeElement">
<Columns>
<DxGridDataColumn FieldName="Id" Caption="ID">
<CellDisplayTemplate Context="cellContext">
@((cellContext.DataItem as EnvelopeDto)?.Id)
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Title" Caption="Titel">
<CellDisplayTemplate Context="cellContext">
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Status" Caption="Status">
<CellDisplayTemplate Context="cellContext">
@{
var envelope = cellContext.DataItem as EnvelopeDto;
if (envelope != null) {
var statusInfo = GetStatusInfo(envelope.Status);
<div class="status-badge status-badge--@statusInfo.CssClass">
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
@statusInfo.Label
</div>
}
}
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger">
<CellDisplayTemplate Context="cellContext">
@{
var envelope = cellContext.DataItem as EnvelopeDto;
if (envelope != null) {
var receivers = envelope.EnvelopeReceivers ?? new List<EnvelopeReceiverSimpleDto>();
var signed = receivers.Count(r => r.Signed);
var total = receivers.Count;
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 0.875rem; color: #6b7280;">
@signed / @total unterschrieben
</span>
@if (total > 0) {
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
</div>
}
</div>
}
}
</CellDisplayTemplate>
</DxGridDataColumn>
</Columns>
<DetailRowTemplate Context="detailContext">
<div style="padding: 1rem; background: #f9fafb;">
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
@{
var envelope = detailContext.DataItem as EnvelopeDto;
if (envelope?.EnvelopeReceivers?.Any() == true) {
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
@foreach (var receiver in envelope.EnvelopeReceivers) {
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
<span class="receiver-badge receiver-badge--@(receiver.Signed ? "signed" : "unsigned")" style="min-width: 100px;">
@if (receiver.Signed) {
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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>
<span>Unterschrieben</span>
} else {
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>
<span>Ausstehend</span>
}
</span>
<div style="flex: 1; font-size: 0.875rem;">
<strong style="color: #1f2937;">@receiver.Name</strong>
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Email</span>
</div>
</div>
}
</div>
} else {
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
}
}
</div>
</DetailRowTemplate>
</DxGrid>
}
</div>
</div>
}
</div>
</div>
@code {
private IEnumerable<EnvelopeDto>? _allEnvelopes;
private IEnumerable<EnvelopeDto>? _activeEnvelopes;
private IEnumerable<EnvelopeDto>? _completedEnvelopes;
private EnvelopeDto? _selectedEnvelope;
private string _activeTab = "active";
private bool _isLoading = true;
private bool _isLoggingOut = false;
private string? _errorMessage;
private DxGrid? _gridActive;
private DxGrid? _gridCompleted;
}
protected override async Task OnInitializedAsync()
{
var hasAccess = await AuthService.CheckSenderAsync();
if (!hasAccess)
{
Navigation.NavigateTo($"/sender/login");
return;
}
await LoadEnvelopesAsync();
}
async Task LoadEnvelopesAsync()
{
_isLoading = true;
_errorMessage = null;
await InvokeAsync(StateHasChanged);
try
{
_allEnvelopes = await EnvelopeService.GetAsync() ?? [];
// Split into active and completed based on status
var envelopes = _allEnvelopes.ToList();
_activeEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsActive()).ToList();
_completedEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsCompleted()).ToList();
await JSRuntime.InvokeVoidAsync("console.log", $"Loaded {_activeEnvelopes.Count()} active and {_completedEnvelopes.Count()} completed envelopes");
}
catch (Exception ex)
{
_errorMessage = ex.Message;
await JSRuntime.InvokeVoidAsync("console.error", "Fehler beim Laden der Umschläge:", ex.ToString());
}
finally
{
_isLoading = false;
await InvokeAsync(StateHasChanged);
}
}
async Task RefreshEnvelopes()
{
await LoadEnvelopesAsync();
}
void CreateEnvelope()
{
// TODO: Navigate to envelope creation page
JSRuntime.InvokeVoidAsync("console.log", "Create envelope clicked - not yet implemented");
}
void EditEnvelope()
{
if (_selectedEnvelope == null) return;
// TODO: Navigate to envelope editor
JSRuntime.InvokeVoidAsync("console.log", $"Edit envelope {_selectedEnvelope.Id} clicked - not yet implemented");
}
void DeleteEnvelope()
{
if (_selectedEnvelope == null) return;
// TODO: Show delete confirmation dialog
JSRuntime.InvokeVoidAsync("console.log", $"Delete envelope {_selectedEnvelope.Id} clicked - not yet implemented");
}
async Task LogoutAsync()
{
_isLoggingOut = true;
await InvokeAsync(StateHasChanged);
await AuthService.LogoutSenderAsync();
Navigation.NavigateTo("/sender/login", forceLoad: true);
}
bool IsEnvelopeSent(EnvelopeDto envelope)
{
var status = (EnvelopeStatus)envelope.Status;
return status >= EnvelopeStatus.EnvelopeQueued;
}
(string Label, string CssClass, string DotColor) GetStatusInfo(int statusCode)
{
var status = (EnvelopeStatus)statusCode;
return status switch
{
EnvelopeStatus.EnvelopePartlySigned => ("Teilweise unterschrieben", "partly-signed", "green"),
EnvelopeStatus.EnvelopeQueued => ("In Warteschlange", "queued", "orange"),
EnvelopeStatus.EnvelopeSent => ("Gesendet", "sent", "orange"),
EnvelopeStatus.EnvelopeCompletelySigned => ("Vollständig unterschrieben", "completed", "green"),
EnvelopeStatus.EnvelopeDeleted => ("Gelöscht", "deleted", "red"),
EnvelopeStatus.EnvelopeRejected => ("Abgelehnt", "rejected", "red"),
EnvelopeStatus.EnvelopeWithdrawn => ("Zurückgezogen", "withdrawn", "red"),
EnvelopeStatus.EnvelopeCreated => ("Erstellt", "created", "blue"),
EnvelopeStatus.EnvelopeSaved => ("Gespeichert", "saved", "blue"),
_ => ("Unbekannt", "unknown", "blue")
};
}
void OnCustomizeElement(GridCustomizeElementEventArgs e)
{
// Future: Add custom row coloring based on status if needed
}
void OnSelectedEnvelopeChanged(object envelope)
{
_selectedEnvelope = envelope as EnvelopeDto;
}
}

View File

@@ -6,6 +6,7 @@
@using DevExpress.Utils
@using DevExpress.XtraPrinting
@using DevExpress.XtraPrinting.Drawing
@using EnvelopeGenerator.Application.Common.Dto
@using Microsoft.JSInterop
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
@@ -301,7 +302,10 @@ Shown="OnPopupShownAsync">
bool IsLoggingOut;
IReadOnlyList<AnnotationDto> _annotations = [];
IEnumerable<int> AnnotationPages => _annotations.Select(a => a.Page).Distinct().OrderBy(p => p);
IEnumerable<int> AnnotationPages => _annotations
.Select(a => a.Page ?? throw new InvalidOperationException($"Annotation page is missing for annotation ID {a.Id}. Annotation details: X={a.X}, Y={a.Y}"))
.Distinct()
.OrderBy(p => p);
EnvelopeReceiverDto? _envelopeReceiver;
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
SignatureCapture? _capturedSignature;

View File

@@ -7,6 +7,9 @@ using EnvelopeGenerator.ReceiverUI.Options;
using DevExpress.XtraReports.Services;
using DevExpress.Blazor.Reporting;
using DevExpress.XtraReports.Web.Extensions;
using EnvelopeGenerator.Application.Resources;
using Microsoft.Extensions.Localization;
using System.Globalization;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
@@ -20,9 +23,14 @@ builder.Services.AddScoped<DocumentService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<AnnotationService>();
builder.Services.AddScoped<EnvelopeReceiverService>();
builder.Services.AddScoped<SignatureService>();
builder.Services.AddScoped<DocReceiverElementService>();
builder.Services.AddScoped<SignatureCacheService>();
builder.Services.AddSingleton<AppVersionService>();
builder.Services.AddScoped<EnvelopeService>();
builder.Services.AddScoped<CultureService>();
// Localization services
builder.Services.AddLocalization();
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
@@ -41,5 +49,30 @@ builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
var host = builder.Build();
// ⚠️ IMPORTANT: BLAZOR WASM-SPECIFIC CULTURE INITIALIZATION
// This approach sets DefaultThreadCurrentCulture globally, which is SAFE for WebAssembly
// because each user runs their own isolated app instance in their browser.
//
// ⚠️ TODO: REMOVE/REFACTOR WHEN MIGRATING TO BLAZOR SERVER/AUTO
// In Server/Auto render modes, this is DANGEROUS because:
// - Server runs a single shared instance for all users
// - Setting global culture affects ALL connected users simultaneously
// - Race conditions and culture conflicts will occur
//
// Migration Guide:
// - Option 1: Use RequestLocalizationMiddleware for per-request culture
// - Option 2: Use CascadingParameter with per-circuit culture state
// - See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization
//
// Related files to update on migration:
// - LanguageSelector.razor (remove manual culture setting)
// - App.razor (may need CascadingValue for culture)
// - Startup/Program.cs (add middleware)
var cultureService = host.Services.GetRequiredService<CultureService>();
var culture = await cultureService.InitializeCultureAsync();
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
await host.RunAsync();

View File

@@ -2,7 +2,7 @@
"profiles": {
"EnvelopeGenerator.ReceiverUI": {
"commandName": "Project",
"launchBrowser": true,
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},

View File

@@ -1,5 +1,6 @@
using System.Net.Http.Json;
using System.Text.Json;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.ReceiverUI.Models;
using EnvelopeGenerator.ReceiverUI.Options;
using Microsoft.Extensions.Options;

View File

@@ -58,6 +58,16 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
return response.IsSuccessStatusCode;
}
/// <summary>
/// Checks whether the current user holds a valid receiver token for the given envelope key.
/// Calls GET /api/auth/check/envelope/{envelopeKey}.
/// </summary>
public async Task<bool> CheckSenderAsync(CancellationToken cancel = default)
{
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check", cancel);
return response.StatusCode == HttpStatusCode.OK;
}
/// <summary>
/// Authenticates a sender user with username and password.
/// Calls POST /api/auth?cookie=true with JSON body.
@@ -78,4 +88,16 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
_ => SenderLoginResult.Error
};
}
/// <summary>
/// Logs out the sender user by removing the authentication cookie.
/// Calls POST /api/auth/logout.
/// </summary>
public async Task<bool> LogoutSenderAsync(CancellationToken cancel = default)
{
var response = await http.PostAsync(
$"{_api.BaseUrl}/api/auth/logout",
null, cancel);
return response.IsSuccessStatusCode;
}
}

View File

@@ -0,0 +1,74 @@
using System.Globalization;
using Microsoft.JSInterop;
namespace EnvelopeGenerator.ReceiverUI.Services;
/// <summary>
/// Service for managing application culture/localization.
/// </summary>
public class CultureService
{
private readonly IJSRuntime _jsRuntime;
private const string CULTURE_KEY = "AppCulture";
public CultureService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
/// <summary>
/// Gets the list of supported cultures.
/// </summary>
public static CultureInfo[] SupportedCultures { get; } = new[]
{
new CultureInfo("de-DE"),
new CultureInfo("en-US"),
new CultureInfo("fr-FR")
};
/// <summary>
/// Sets the application culture and stores it in localStorage.
/// </summary>
public async Task SetCultureAsync(string culture)
{
if (!SupportedCultures.Any(c => c.Name == culture))
throw new ArgumentException($"Culture '{culture}' is not supported.", nameof(culture));
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", CULTURE_KEY, culture);
}
/// <summary>
/// Gets the stored culture from localStorage.
/// </summary>
public async Task<string?> GetCultureAsync()
{
try
{
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", CULTURE_KEY);
}
catch
{
return null;
}
}
/// <summary>
/// Initializes the culture from localStorage or browser settings.
/// </summary>
public async Task<CultureInfo> InitializeCultureAsync()
{
var storedCulture = await GetCultureAsync();
if (!string.IsNullOrEmpty(storedCulture) &&
SupportedCultures.Any(c => c.Name == storedCulture))
{
return new CultureInfo(storedCulture);
}
// Fallback to browser culture or default
var browserCulture = CultureInfo.CurrentCulture.Name;
var matchedCulture = SupportedCultures.FirstOrDefault(c => c.Name == browserCulture);
return matchedCulture ?? SupportedCultures[0]; // Default to German
}
}

View File

@@ -6,13 +6,13 @@ using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.ReceiverUI.Services;
public class SignatureService(HttpClient http, IOptions<ApiOptions> apiOptions)
public class DocReceiverElementService(HttpClient http, IOptions<ApiOptions> apiOptions)
{
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
{
var url = $"{apiOptions.Value.BaseUrl}/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
var url = $"{apiOptions.Value.BaseUrl}/api/DocReceiverElement/{Uri.EscapeDataString(envelopeKey)}";
var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode)

View File

@@ -0,0 +1,72 @@
using System.Net.Http.Json;
using System.Text.Json;
using EnvelopeGenerator.ReceiverUI.Models;
using EnvelopeGenerator.ReceiverUI.Options;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.ReceiverUI.Services;
/// <summary>
/// Retrieves <see cref="EnvelopeDto"/>s from the API.
/// </summary>
public class EnvelopeService
{
private readonly HttpClient _http;
private readonly ApiOptions _apiOptions;
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
public EnvelopeService(HttpClient http, IOptions<ApiOptions> apiOptions)
{
_http = http;
_apiOptions = apiOptions.Value;
}
/// <summary>
/// Fetches envelopes from the API with optional filters.
/// </summary>
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
public async Task<IEnumerable<EnvelopeDto>?> GetAsync(
int? id = null,
string? uuid = null,
bool? onlyActive = null,
bool? onlyCompleted = null,
CancellationToken cancel = default)
{
var baseUrl = $"{_apiOptions.BaseUrl}/api/Envelope";
var queryParams = new Dictionary<string, string?>();
if (id.HasValue)
{
queryParams["Id"] = id.Value.ToString();
}
if (!string.IsNullOrEmpty(uuid))
{
queryParams["Uuid"] = uuid;
}
if (onlyActive.HasValue)
{
queryParams["OnlyActive"] = onlyActive.Value.ToString();
}
if (onlyCompleted.HasValue)
{
queryParams["OnlyCompleted"] = onlyCompleted.Value.ToString();
}
var url = QueryHelpers.AddQueryString(baseUrl, queryParams);
var response = await _http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode)
{
var statusCode = (int)response.StatusCode;
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
throw new HttpRequestException(
$"Failed to load envelopes. Status: {statusCode} ({reasonPhrase})",
null,
response.StatusCode);
}
return await response.Content.ReadFromJsonAsync<IEnumerable<EnvelopeDto>>(_jsonOptions, cancel);
}
}

View File

@@ -0,0 +1,80 @@
@using System.Globalization
@using EnvelopeGenerator.ReceiverUI.Services
@inject IJSRuntime JSRuntime
@inject NavigationManager Navigation
@inject CultureService CultureService
<div class="language-selector">
<button class="language-selector__trigger" @onclick="ToggleDropdown" aria-label="Select Language">
<span class="fi fi-@GetFlagCode(CurrentCulture)"></span>
<span class="language-selector__arrow">@GetLanguageName(CurrentCulture)</span>
</button>
@if (isOpen)
{
<div class="language-selector__dropdown">
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("de-DE"))">
<span class="fi fi-de"></span>
<span>Deutsch</span>
</button>
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("en-US"))">
<span class="fi fi-us"></span>
<span>English</span>
</button>
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("fr-FR"))">
<span class="fi fi-fr"></span>
<span>Français</span>
</button>
</div>
}
</div>
@code {
private bool isOpen = false;
private string CurrentCulture => CultureInfo.CurrentCulture.Name;
private void ToggleDropdown()
{
isOpen = !isOpen;
}
private async Task ChangeLanguageAsync(string culture)
{
if (CultureInfo.CurrentCulture.Name != culture)
{
await CultureService.SetCultureAsync(culture);
// Set culture without page reload
var cultureInfo = new CultureInfo(culture);
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
// Navigate without reload to trigger re-render
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
}
isOpen = false;
}
private string GetFlagCode(string culture)
{
return culture switch
{
"de-DE" => "de",
"en-US" => "us",
"fr-FR" => "fr",
_ => "de"
};
}
private string GetLanguageName(string culture)
{
return culture switch
{
"de-DE" => "Deutsch",
"en-US" => "English",
"fr-FR" => "Français",
_ => "Deutsch"
};
}
}

View File

@@ -8,9 +8,12 @@
</article>
</main>
<footer class="receiver-footer">
<span>&copy; SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
<span class="receiver-footer__sep">&#124;</span>
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
<div class="receiver-footer__content">
<span>&copy; SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
<span class="receiver-footer__sep">&#124;</span>
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
</div>
<LanguageSelector />
</footer>
</div>

View File

@@ -15,3 +15,18 @@ article {
padding: 0 !important;
margin: 0 !important;
}
.receiver-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
gap: 1rem;
}
.receiver-footer__content {
display: flex;
align-items: center;
gap: 0.5rem;
}

View File

@@ -365,4 +365,75 @@ article {
.receiver-footer__sep {
opacity: 0.4;
}
}
/* ── Language Selector (Footer) ──────────────────────────────────────────── */
.language-selector {
position: relative;
display: inline-block;
}
.language-selector__trigger {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.25rem 0.5rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: rgba(255, 255, 255, 0.9);
cursor: pointer;
font-size: 1rem;
transition: all 0.2s ease;
}
.language-selector__trigger:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
.language-selector__arrow {
font-size: 0.6rem;
transition: transform 0.2s ease;
opacity: 0.7;
}
.language-selector__dropdown {
position: absolute;
bottom: calc(100% + 0.5rem);
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 160px;
z-index: 1000;
overflow: hidden;
}
.language-selector__option {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.65rem 1rem;
background: white;
border: none;
color: #333;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s ease;
text-align: left;
}
.language-selector__option:hover {
background-color: #f5f5f5;
}
.language-selector__option .fi {
font-size: 1.25rem;
}
.language-selector__option span:last-child {
font-weight: 500;
}

View File

@@ -0,0 +1,297 @@
.sender-dashboard-layout {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%);
}
.sender-action-bar {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-bottom: 3px solid rgba(126, 34, 206, 0.3);
padding: 1rem 2rem;
flex-shrink: 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.sender-action-bar__inner {
max-width: 1600px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
}
.sender-title-section {
display: flex;
align-items: center;
gap: 1rem;
}
.sender-logo svg {
filter: drop-shadow(0 2px 4px rgba(126, 34, 206, 0.3));
color: #7e22ce;
}
.sender-title {
font-size: 1.25rem;
font-weight: 700;
color: #1e293b;
letter-spacing: -0.025em;
}
.sender-toolbar {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.sender-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.125rem;
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
border: 1px solid rgba(126, 34, 206, 0.2);
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
color: #1e293b;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.sender-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
border-color: rgba(126, 34, 206, 0.4);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2);
}
.sender-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
background: rgba(0, 0, 0, 0.02);
border-color: rgba(0, 0, 0, 0.1);
}
.sender-btn--primary {
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
border-color: transparent;
color: white;
}
.sender-btn--primary:hover:not(:disabled) {
background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(126, 34, 206, 0.3);
}
.sender-btn--danger {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%);
border-color: rgba(239, 68, 68, 0.3);
color: #dc2626;
}
.sender-btn--danger:hover:not(:disabled) {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
border-color: transparent;
color: white;
}
.sender-btn--logout {
padding: 0.5rem;
min-width: 38px;
}
.sender-content {
flex: 1;
min-height: 0;
padding: 1.5rem;
position: relative;
overflow: auto;
}
.sender-grid-container {
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1);
overflow: hidden;
position: relative;
max-width: 1600px;
margin: 0 auto;
}
.sender-grid-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #7e22ce 0%, #2a5298 100%);
z-index: 1;
border-radius: 16px 16px 0 0;
}
.sender-tabs {
display: flex;
border-bottom: 2px solid rgba(126, 34, 206, 0.1);
padding: 0 2rem;
background: rgba(126, 34, 206, 0.02);
}
.sender-tab {
padding: 1rem 1.5rem;
font-size: 0.875rem;
font-weight: 600;
color: #6b7280;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.sender-tab:hover {
color: #7e22ce;
background: rgba(126, 34, 206, 0.05);
}
.sender-tab--active {
color: #7e22ce;
border-bottom-color: #7e22ce;
background: white;
}
.sender-grid-wrapper {
padding: 1.5rem 2rem 2rem;
}
/* Hide DevExpress empty cells */
.dxbl-grid-empty-cell {
display: none !important;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.625rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.status-badge--partly-signed,
.status-badge--completed {
background: rgba(129, 199, 132, 0.15);
color: #2e7d32;
}
.status-badge--queued,
.status-badge--sent {
background: rgba(255, 183, 77, 0.15);
color: #e65100;
}
.status-badge--deleted,
.status-badge--rejected,
.status-badge--withdrawn {
background: rgba(229, 115, 115, 0.15);
color: #c62828;
}
.status-badge--created,
.status-badge--saved {
background: rgba(100, 181, 246, 0.15);
color: #1565c0;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.status-dot--green {
background: #81c784;
}
.status-dot--orange {
background: #ffb74d;
}
.status-dot--red {
background: #e57373;
}
.status-dot--blue {
background: #64b5f6;
}
.receiver-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
background: #f3f4f6;
border-radius: 4px;
font-size: 0.75rem;
color: #374151;
white-space: nowrap;
}
.receiver-badge--signed {
background: rgba(129, 199, 132, 0.15);
color: #2e7d32;
}
.receiver-badge--unsigned {
background: rgba(229, 115, 115, 0.15);
color: #c62828;
}
@@media (max-width: 768px) {
.sender-action-bar {
padding: 1rem 1.25rem;
}
.sender-action-bar__inner {
flex-wrap: wrap;
}
.sender-toolbar {
width: 100%;
justify-content: flex-start;
}
.sender-title {
font-size: 1.125rem;
}
.sender-content {
padding: 0.75rem;
}
.sender-grid-wrapper {
padding: 1rem;
}
.sender-tabs {
padding: 0 1rem;
overflow-x: auto;
}
.sender-tab {
padding: 0.875rem 1rem;
font-size: 0.813rem;
}
}

View File

@@ -12,6 +12,7 @@
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="EnvelopeGenerator.ReceiverUI.styles.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" />
<style type="text/css">
.splash-screen {
display: flex;

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Data {
namespace EnvelopeGenerator.Server.Client.Data {
public class Adjustment
{
public static Adjustment CreateBalanceForward(DateTime dt, int random)

View File

@@ -1,7 +1,7 @@
using DevExpress.DataAccess.Sql;
using DevExpress.DataAccess.Sql.DataApi;
namespace EnvelopeGenerator.WebUI.Client.Data {
namespace EnvelopeGenerator.Server.Client.Data {
public class Customer {
static List<Customer> currentCustomers = new List<Customer>();

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Data {
namespace EnvelopeGenerator.Server.Client.Data {
public class DataItem {
static readonly string[] accountType = new string[] { "Energy", "Manufacturing", "Estate", "Food", "Services" };
public string CustomerID { get; set; }

View File

@@ -1,6 +1,6 @@
using System.Collections;
namespace EnvelopeGenerator.WebUI.Client.Data {
namespace EnvelopeGenerator.Server.Client.Data {
public class DataItemList : IList<DataItem>, IList {
readonly int rowCount;

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Data {
namespace EnvelopeGenerator.Server.Client.Data {
class DeterministicRandom {
const int randomCount = 10000;
static readonly int[] deterministicRandomNumbers;

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Data {
namespace EnvelopeGenerator.Server.Client.Data {
public struct Term {
public static readonly Term[] Terms = new Term[] {
new Term("Payment seven days after invoice date" ),

View File

@@ -17,6 +17,7 @@
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />

View File

@@ -1,4 +1,4 @@
@using EnvelopeGenerator.WebUI.Client.Services;
@using EnvelopeGenerator.Server.Client.Services;
@inherits LayoutComponentBase
<div class="page">
@@ -15,13 +15,14 @@
</div>
@code {
[Inject] HttpClient Http { get; set; }
[Inject] IHttpClientFactory HttpClientFactory { get; set; } = default!;
List<string> RequiredFonts = new() {
"opensans.ttf"
};
protected async override Task OnInitializedAsync() {
await FontLoader.LoadFonts(Http, RequiredFonts);
await FontLoader.LoadFonts(HttpClientFactory, RequiredFonts);
await base.OnInitializedAsync();
}
}

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Models;
namespace EnvelopeGenerator.Server.Client.Models;
/// <summary>
/// Represents a pre-assigned signature annotation position on a specific page.

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Models.Constants
namespace EnvelopeGenerator.Server.Client.Models.Constants
{
public enum SenderAppType
{

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Models.Constants;
namespace EnvelopeGenerator.Server.Client.Models.Constants;
/// <summary>
/// Represents the unit of measurement for coordinate values in signature positioning.

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Models;
namespace EnvelopeGenerator.Server.Client.Models;
/// <summary>
/// Client-side model for the envelope receiver returned by

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Models;
namespace EnvelopeGenerator.Server.Client.Models;
/// <summary>
/// Represents a captured signature with metadata created by the receiver in the signature popup.

View File

@@ -1,6 +1,6 @@
using EnvelopeGenerator.WebUI.Client.Models.Constants;
using EnvelopeGenerator.Server.Client.Models.Constants;
namespace EnvelopeGenerator.WebUI.Client.Models;
namespace EnvelopeGenerator.Server.Client.Models;
/// <summary>
/// Represents a signature position on a PDF page.

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Options;
namespace EnvelopeGenerator.Server.Client.Options;
public class ApiOptions
{

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Options;
namespace EnvelopeGenerator.Server.Client.Options;
public class PdfViewerOptions
{

View File

@@ -1,6 +1,6 @@
@page "/envelope/login/{EnvelopeKey}"
@rendermode InteractiveWebAssembly
@using EnvelopeGenerator.WebUI.Client.Services
@using EnvelopeGenerator.Server.Client.Services
@inject AuthService AuthService
@inject NavigationManager Navigation
@@ -12,7 +12,7 @@
<div class="card-header text-white text-center py-4 border-0" style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;">
<div class="mb-2">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
</svg>
</div>
<h5 class="mb-0 fw-semibold">Dokument öffnen</h5>
@@ -25,31 +25,36 @@
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
</p>
@if (LoginResult == EnvelopeLoginResult.NotFound) {
@if (LoginResult == EnvelopeLoginResult.NotFound)
{
<div class="alert alert-warning d-flex align-items-start gap-2 py-2" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</svg>
<div>
<strong>Dokument nicht gefunden.</strong><br />
<span style="font-size:0.85rem;">Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</span>
</div>
</div>
} else if (LoginResult == EnvelopeLoginResult.InvalidCode) {
}
else if (LoginResult == EnvelopeLoginResult.InvalidCode)
{
<div class="alert alert-danger d-flex align-items-start gap-2 py-2" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
</svg>
<div>
<strong>Ungültiger Zugangscode.</strong><br />
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
</div>
</div>
} else if (LoginResult == EnvelopeLoginResult.Error) {
}
else if (LoginResult == EnvelopeLoginResult.Error)
{
<div class="alert alert-secondary d-flex align-items-start gap-2 py-2" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" 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"/>
<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>
<strong>Serverfehler.</strong><br />
@@ -66,7 +71,7 @@
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" viewBox="0 0 16 16">
<path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1.837 1.337A3.5 3.5 0 0 1 3.5 11.5zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
<path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1.837 1.337A3.5 3.5 0 0 1 3.5 11.5zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z" />
</svg>
</span>
<input id="login-access-code"
@@ -83,17 +88,20 @@
style="border-left: none;"
tabindex="-1"
@onclick="() => ShowCode = !ShowCode">
@if (ShowCode) {
@if (ShowCode)
{
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z"/>
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z"/>
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z" />
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z" />
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z" />
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z" />
</svg>
} else {
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z" />
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" />
</svg>
}
</button>
@@ -104,12 +112,15 @@
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
@onclick="SubmitAsync"
disabled="@(IsLoading || string.IsNullOrWhiteSpace(AccessCode))">
@if (IsLoading) {
@if (IsLoading)
{
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span>Überprüfen …</span>
} else {
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
</svg>
<span>Dokument öffnen</span>
}
@@ -132,12 +143,14 @@
bool IsLoading;
EnvelopeLoginResult? LoginResult;
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e)
{
if (e.Key == "Enter")
await SubmitAsync();
}
async Task SubmitAsync() {
async Task SubmitAsync()
{
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
IsLoading = true;
@@ -146,7 +159,8 @@
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
if (result == EnvelopeLoginResult.Success) {
if (result == EnvelopeLoginResult.Success)
{
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
return;
}
@@ -156,3 +170,4 @@
await InvokeAsync(StateHasChanged);
}
}

View File

@@ -1,6 +1,6 @@
@page "/sender/login"
@rendermode InteractiveWebAssembly
@using EnvelopeGenerator.WebUI.Client.Services
@using EnvelopeGenerator.Server.Client.Services
@inject AuthService AuthService
@inject NavigationManager Navigation

View File

@@ -1,5 +1,5 @@
using DevExpress.XtraReports.UI;
namespace EnvelopeGenerator.WebUI.Client.PredefinedReports {
namespace EnvelopeGenerator.Server.Client.PredefinedReports {
public class Report : XtraReport {
private TopMarginBand topMarginBand1;
private XRPageInfo xrPageInfo4;
@@ -1081,7 +1081,7 @@ namespace EnvelopeGenerator.WebUI.Client.PredefinedReports {
objectConstructorInfo1.Parameters.AddRange(new DevExpress.DataAccess.ObjectBinding.Parameter[] {
parameter1});
this.objectDataSource1.Constructor = objectConstructorInfo1;
this.objectDataSource1.DataSource = typeof(EnvelopeGenerator.WebUI.Client.Data.DataItemList);
this.objectDataSource1.DataSource = typeof(EnvelopeGenerator.Server.Client.Data.DataItemList);
this.objectDataSource1.Name = "objectDataSource1";
//
// Title

View File

@@ -1,6 +1,6 @@
using DevExpress.XtraReports.UI;
namespace EnvelopeGenerator.WebUI.Client.PredefinedReports {
namespace EnvelopeGenerator.Server.Client.PredefinedReports {
public static class ReportsFactory
{
public static readonly Dictionary<string, Func<XtraReport>> Reports = new() {

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using EnvelopeGenerator.WebUI.Client.Services;
using EnvelopeGenerator.WebUI.Client.Options;
using EnvelopeGenerator.Server.Client.Services;
using EnvelopeGenerator.Server.Client.Options;
using DevExpress.Blazor.Reporting;
using DevExpress.XtraReports.Web.Extensions;
using DevExpress.DataAccess.Web;
@@ -8,14 +8,19 @@ using DevExpress.XtraReports.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// HTTP Client (uses WebUI's YARP proxy)
builder.Services.AddScoped(sp => new HttpClient {
// Named HttpClient for API calls (both for services and DevExpress components)
builder.Services.AddHttpClient("EnvelopeGenerator.Server", client =>
{
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
});
// Default HttpClient (DevExpress PdfViewer requires this)
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
// Configuration Options
builder.Services.Configure<ApiOptions>(opts =>
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
builder.Services.Configure<PdfViewerOptions>(opts =>
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
@@ -41,8 +46,8 @@ builder.Services.AddScoped<IDataSourceWizardJsonConnectionStorage, CustomDataSou
builder.Services.AddScoped<IJsonDataConnectionProviderFactory, CustomJsonDataConnectionProviderFactory>();
builder.Services.AddScoped<IObjectDataSourceWizardTypeProvider, ObjectDataSourceWizardCustomTypeProvider>();
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.WebUI.Client.Data.DataItemList));
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.WebUI.Client.PredefinedReports.Report));
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.Server.Client.Data.DataItemList));
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.Server.Client.PredefinedReports.Report));
builder.Services.AddSingleton<InMemoryReportStorageWebExtension>();
builder.Services.AddSingleton<ReportStorageWebExtension>(sp => sp.GetRequiredService<InMemoryReportStorageWebExtension>());
@@ -51,5 +56,5 @@ builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
var host = builder.Build();
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
await FontLoader.LoadFonts(host.Services.GetRequiredService<IHttpClientFactory>(), new List<string> { "opensans.ttf" });
await host.RunAsync();

View File

@@ -0,0 +1,29 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using EnvelopeGenerator.Server.Client.Models;
namespace EnvelopeGenerator.Server.Client.Services;
/// <summary>
/// Retrieves annotation positions from the API.
/// Uses relative paths (/api/Annotation/{envelopeKey}).
/// </summary>
[Obsolete("Use SignatureService.")]
public class AnnotationService(IHttpClientFactory httpClientFactory)
{
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
public async Task<IReadOnlyList<AnnotationDto>> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var url = $"/api/Annotation/{Uri.EscapeDataString(envelopeKey)}";
var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode)
return [];
var result = await response.Content.ReadFromJsonAsync<List<AnnotationDto>>(_jsonOptions, cancel);
return result ?? [];
}
}

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
/// <summary>
/// Provides application version for cache busting static assets.

View File

@@ -1,17 +1,15 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using EnvelopeGenerator.WebUI.Client.Options;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
public enum SenderLoginResult { Success, InvalidCredentials, Error }
public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
public class AuthService(IHttpClientFactory httpClientFactory)
{
private readonly ApiOptions _api = apiOptions.Value;
/// <summary>
/// Checks whether the current user holds a valid receiver token for the given envelope key.
@@ -19,7 +17,8 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// </summary>
public async Task<bool> CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default)
{
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel);
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.GetAsync($"/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel);
return response.StatusCode == HttpStatusCode.OK;
}
@@ -30,11 +29,12 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// </summary>
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var form = new MultipartFormDataContent();
form.Add(new StringContent(accessCode), "AccessCode");
var response = await http.PostAsync(
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
$"/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
form, cancel);
return response.StatusCode switch
@@ -52,8 +52,9 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// </summary>
public async Task<bool> LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.PostAsync(
$"{_api.BaseUrl}/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
$"/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
null, cancel);
return response.IsSuccessStatusCode;
}
@@ -65,10 +66,11 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// </summary>
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password, CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var requestBody = new { username, password };
var response = await http.PostAsJsonAsync(
$"{_api.BaseUrl}/api/auth?cookie=true",
$"/api/auth?cookie=true",
requestBody, cancel);
return response.StatusCode switch

View File

@@ -2,7 +2,7 @@
using DevExpress.DataAccess.Web;
using DevExpress.DataAccess.Wizard.Services;
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
public class CustomDataSourceWizardJsonDataConnectionStorage : IDataSourceWizardJsonConnectionStorage
{

View File

@@ -1,6 +1,6 @@
using DevExpress.DataAccess.Json;
using DevExpress.DataAccess.Web;
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
public class CustomJsonDataConnectionProviderFactory : IJsonDataConnectionProviderFactory {
public IJsonDataConnectionProviderService Create() {

View File

@@ -1,8 +1,8 @@
using DevExpress.XtraReports.UI;
using DevExpress.XtraReports.Services;
using EnvelopeGenerator.WebUI.Client.PredefinedReports;
using EnvelopeGenerator.Server.Client.PredefinedReports;
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
public class CustomReportProvider : IReportProviderAsync {
private readonly InMemoryReportStorageWebExtension reportStorage;

View File

@@ -1,13 +1,10 @@
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.Options;
using EnvelopeGenerator.WebUI.Client.Options;
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
public class DocumentService(HttpClient http, IOptions<ApiOptions> apiOptions)
public class DocumentService(IHttpClientFactory httpClientFactory)
{
private readonly ApiOptions _api = apiOptions.Value;
/// <summary>
/// Fetches the PDF bytes for the given envelope key from the API.
@@ -16,7 +13,8 @@ public class DocumentService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
public async Task<byte[]?> GetDocumentAsync(string envelopeKey, CancellationToken cancel = default)
{
var response = await http.GetAsync($"{_api.BaseUrl}/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.GetAsync($"/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
if (!response.IsSuccessStatusCode)
{

View File

@@ -1,17 +1,16 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using EnvelopeGenerator.WebUI.Client.Models;
using EnvelopeGenerator.WebUI.Client.Options;
using Microsoft.Extensions.Options;
using EnvelopeGenerator.Server.Client.Models;
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
/// <summary>
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
/// from <c>GET api/EnvelopeReceiver/{envelopeKey}</c>.
/// from <c>GET /api/EnvelopeReceiver/{envelopeKey}</c>.
/// </summary>
public class EnvelopeReceiverService(HttpClient http, IOptions<ApiOptions> apiOptions)
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
{
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
@@ -22,7 +21,8 @@ public class EnvelopeReceiverService(HttpClient http, IOptions<ApiOptions> apiOp
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
public async Task<EnvelopeReceiverDto?> GetAsync(string envelopeKey, CancellationToken cancel = default)
{
var url = $"{apiOptions.Value.BaseUrl}/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}";
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var url = $"/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}";
var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode)

View File

@@ -0,0 +1,17 @@
using DevExpress.Drawing;
namespace EnvelopeGenerator.Server.Client.Services;
public static class FontLoader
{
public static async Task LoadFonts(IHttpClientFactory httpClientFactory, List<string> fontNames)
{
using var httpClient = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
foreach (var fontName in fontNames)
{
var fontBytes = await httpClient.GetByteArrayAsync($"/fonts/{fontName}");
DXFontRepository.Instance.AddFont(fontBytes);
}
}
}

View File

@@ -1,8 +1,8 @@
using DevExpress.XtraReports.UI;
using DevExpress.XtraReports.Web.Extensions;
using EnvelopeGenerator.WebUI.Client.PredefinedReports;
using EnvelopeGenerator.Server.Client.PredefinedReports;
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
public class InMemoryReportStorageWebExtension : ReportStorageWebExtension
{

View File

@@ -1,6 +1,6 @@
using DevExpress.DataAccess.Web;
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
public class ObjectDataSourceWizardCustomTypeProvider : IObjectDataSourceWizardTypeProvider {
public IEnumerable<Type> GetAvailableTypes(string context) {

View File

@@ -1,24 +1,23 @@
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.Extensions.Options;
using EnvelopeGenerator.WebUI.Client.Options;
using EnvelopeGenerator.WebUI.Client.Models;
using EnvelopeGenerator.Server.Client.Models;
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
/// <summary>
/// Client service for managing cached signatures via API.
/// </summary>
public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOptions)
public class SignatureCacheService(IHttpClientFactory httpClientFactory)
{
private readonly ApiOptions _api = apiOptions.Value;
public async Task SaveSignatureAsync(
string envelopeKey,
SignatureCaptureDto signature,
CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.PostAsJsonAsync(
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
$"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
signature,
cancel);
@@ -33,8 +32,9 @@ public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOpti
string envelopeKey,
CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.GetAsync(
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
$"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
cancel);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
@@ -53,8 +53,9 @@ public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOpti
string envelopeKey,
CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.DeleteAsync(
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
$"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
cancel);
if (!response.IsSuccessStatusCode)

View File

@@ -1,18 +1,18 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using EnvelopeGenerator.WebUI.Client.Models;
using EnvelopeGenerator.WebUI.Client.Options;
using Microsoft.Extensions.Options;
using EnvelopeGenerator.Server.Client.Models;
namespace EnvelopeGenerator.WebUI.Client.Services;
namespace EnvelopeGenerator.Server.Client.Services;
public class SignatureService(HttpClient http, IOptions<ApiOptions> apiOptions)
public class SignatureService(IHttpClientFactory httpClientFactory)
{
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
{
var url = $"{apiOptions.Value.BaseUrl}/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var url = $"/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode)

View File

@@ -7,10 +7,10 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using EnvelopeGenerator.WebUI.Client
@using EnvelopeGenerator.WebUI.Client.Services
@using EnvelopeGenerator.WebUI.Client.Models
@using EnvelopeGenerator.WebUI.Client.Options
@using EnvelopeGenerator.Server.Client
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Server.Client.Models
@using EnvelopeGenerator.Server.Client.Options
@using DevExpress.Blazor
@using DevExpress.Blazor.PdfViewer
@using DevExpress.Blazor.Reporting

View File

@@ -0,0 +1,17 @@
namespace EnvelopeGenerator.Server;
/// <summary>
/// Authentication scheme names for envelope generator.
/// </summary>
public static class AuthScheme
{
/// <summary>
/// Scheme name used for per-envelope receiver JWT authentication.
/// </summary>
public const string Receiver = "EnvelopeGenerator.Server.ReceiverJWT";
/// <summary>
/// Scheme name used for per-envelope sender JWT authentication.
/// </summary>
public const string Sender = "EnvelopeGenerator.Server.SenderJWT";
}

View File

@@ -8,12 +8,12 @@
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/envelope-viewer.css" />
<link rel="stylesheet" href="EnvelopeGenerator.WebUI.styles.css" />
<HeadOutlet @rendermode="InteractiveAuto" />
<link rel="stylesheet" href="EnvelopeGenerator.Server.styles.css" />
<HeadOutlet />
</head>
<body>
<Routes @rendermode="InteractiveAuto" />
<Routes />
<script src="_content/DevExpress.Blazor.Resources/js/preload-script.js"></script>
<script src="js/typed.umd.js"></script>
<script src="js/receiver-signature.js?v=9"></script>

View File

@@ -1,10 +1,10 @@
@page "/envelope/{EnvelopeKey}"
@rendermode InteractiveServer
@using EnvelopeGenerator.WebUI.Client.Models
@using EnvelopeGenerator.WebUI.Client.Models.Constants
@using EnvelopeGenerator.WebUI.Client.Services
@using EnvelopeGenerator.Server.Client.Models
@using EnvelopeGenerator.Server.Client.Models.Constants
@using EnvelopeGenerator.Server.Client.Services
@using Microsoft.Extensions.Options
@using EnvelopeGenerator.WebUI.Client.Options
@using EnvelopeGenerator.Server.Client.Options
@using Microsoft.JSInterop
@using DevExpress.Blazor
@inject DocumentService DocumentService
@@ -14,8 +14,8 @@
@inject IJSRuntime JSRuntime
@inject SignatureService SignatureService
@inject SignatureCacheService SignatureCacheService
@inject EnvelopeGenerator.WebUI.Client.Services.AuthService AuthService
@inject EnvelopeGenerator.WebUI.Client.Services.EnvelopeReceiverService EnvelopeReceiverService
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeReceiverService EnvelopeReceiverService
@inject AppVersionService AppVersion
@inject ILogger<EnvelopeReceiverPage> logger
@implements IAsyncDisposable

Some files were not shown because too many files have changed in this diff Show More