Compare commits
5 Commits
6c142eba08
...
a5e4f97397
| Author | SHA1 | Date | |
|---|---|---|---|
| a5e4f97397 | |||
| 6ca03a50eb | |||
| 96a84ba1a5 | |||
| ec0ea72890 | |||
| 7b912387e7 |
@@ -1,542 +1,351 @@
|
||||
# EnvelopeGenerator — AI Context Reference
|
||||
# EnvelopeGenerator — Current Workspace Context
|
||||
|
||||
## 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.
|
||||
Digital document signing system for senders and receivers.
|
||||
|
||||
**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed)
|
||||
- Senders authenticate, view envelope lists, and manage envelope workflows.
|
||||
- Receivers authenticate per envelope, open PDFs, create signatures, and apply them in the viewer.
|
||||
- The active UI stack is `Blazor Auto` with server-side and WebAssembly render modes.
|
||||
- Primary UI/PDF libraries are `DevExpress` and `PDF.js`.
|
||||
|
||||
---
|
||||
|
||||
## Migration Notice
|
||||
## Active Application Structure
|
||||
|
||||
**EnvelopeGenerator.ReceiverUI ? EnvelopeGenerator.WebUI Migration**
|
||||
### Main Host
|
||||
**Primary active application:** `EnvelopeGenerator.Server`
|
||||
|
||||
The project has been migrated from pure Blazor WebAssembly (`ReceiverUI`) to **Blazor Auto (Server+WASM hybrid)** architecture (`WebUI`) to resolve DevExpress `DxPdfViewer` compatibility issues.
|
||||
`EnvelopeGenerator.Server` is the current runtime host and contains:
|
||||
- Blazor server host
|
||||
- WebAssembly host integration
|
||||
- API controllers
|
||||
- authentication/authorization setup
|
||||
- Swagger/Scalar setup
|
||||
- YARP reverse proxy configuration
|
||||
- DevExpress server-side services
|
||||
- SQL Server distributed cache setup
|
||||
|
||||
**Reason:** DevExpress `DxPdfViewer` requires backend server-side rendering services that are NOT available in pure WebAssembly projects.
|
||||
### Client Project
|
||||
**Client UI project:** `EnvelopeGenerator.Server.Client`
|
||||
|
||||
**New Structure:**
|
||||
- **WebUI** (Server project): Hosts server-side components, YARP proxy, DevExpress backend services
|
||||
- **WebUI.Client** (WASM project): Client-side components, business logic, services
|
||||
This project contains:
|
||||
- WebAssembly-rendered pages
|
||||
- client-side services
|
||||
- client models and options
|
||||
- sender and receiver login flows
|
||||
|
||||
**Migration Details:** See `MIGRATION_CONTEXT.md`
|
||||
### Other Projects
|
||||
- `EnvelopeGenerator.Application` — MediatR/CQRS handlers and business logic
|
||||
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
|
||||
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
|
||||
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
|
||||
- `EnvelopeGenerator.API` — still exists in the solution, but the current merged app host is `EnvelopeGenerator.Server`
|
||||
|
||||
### Legacy / Do Not Touch
|
||||
- `EnvelopeGenerator.Service`
|
||||
- `EnvelopeGenerator.Form`
|
||||
- `EnvelopeGenerator.BBTests`
|
||||
- `EnvelopeGenerator.CommonServices`
|
||||
|
||||
---
|
||||
|
||||
## Deployment Architecture
|
||||
## Current Hosting Model
|
||||
|
||||
**Two Presentation Projects (Both Required):**
|
||||
`EnvelopeGenerator.Server/Program.cs` currently configures:
|
||||
- `AddRazorComponents()` with both interactive server and interactive WebAssembly components
|
||||
- `AddControllers()` and `MapControllers()`
|
||||
- JWT authentication for sender and receiver flows
|
||||
- cookie authentication
|
||||
- authorization policies using `AuthScheme.Sender`, `AuthScheme.Receiver`, `AuthPolicy.Sender`, `AuthPolicy.Receiver`
|
||||
- `AddReverseProxy()` with `yarp.json`
|
||||
- Swagger / OpenAPI / Scalar
|
||||
- distributed SQL Server cache
|
||||
- DevExpress Blazor and DevExpress PDF Viewer server-side services
|
||||
- request localization middleware
|
||||
|
||||
1. **EnvelopeGenerator.API** (ASP.NET Core Web API)
|
||||
- Runs independently (development & production)
|
||||
- Backend services for document management, authentication, signature endpoints
|
||||
- Serves as API endpoint for WebUI
|
||||
|
||||
2. **EnvelopeGenerator.WebUI** (Blazor Auto - Server+WASM Hybrid)
|
||||
- **Server Project (`EnvelopeGenerator.WebUI`):**
|
||||
- **YARP Reverse Proxy** configured via `yarp.json`
|
||||
- Proxies `/api/*` requests to `API:8088`
|
||||
- Hosts server-side components (`@rendermode InteractiveServer`)
|
||||
- DevExpress server-side services (DxPdfViewer backend)
|
||||
- **Client Project (`EnvelopeGenerator.WebUI.Client`):**
|
||||
- Client-side components (`@rendermode InteractiveWebAssembly`)
|
||||
- Business logic services (AuthService, DocumentService, etc.)
|
||||
- WASM runtime
|
||||
|
||||
**Request Flow:**
|
||||
```
|
||||
Client ? WebUI:XXXX (Blazor Auto)
|
||||
?? Server-side Pages (DxPdfViewer)
|
||||
?? Client-side Pages (WASM)
|
||||
?? YARP Proxy: /api/* ? API:8088
|
||||
```
|
||||
|
||||
**Configuration:** `EnvelopeGenerator.WebUI/yarp.json`
|
||||
This means the active app is a **merged UI + API host**.
|
||||
|
||||
---
|
||||
|
||||
## WebUI Route Structure
|
||||
## Reverse Proxy
|
||||
|
||||
### Root Route
|
||||
| Route | File | Location | Render Mode |
|
||||
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||
|
||||
Current YARP usage is focused on **AuthHub forwarding**, not a general `/api/* -> EnvelopeGenerator.API` proxy.
|
||||
|
||||
Configured routes forward:
|
||||
- `POST /api/auth` -> AuthHub `/api/auth/sign-flow`
|
||||
- `POST /api/Auth/envelope-receiver/{key}` -> AuthHub `/api/auth/envelope-receiver/{key}?cookie=true`
|
||||
|
||||
---
|
||||
|
||||
## Active Routes and Files
|
||||
|
||||
### WebAssembly Pages (`EnvelopeGenerator.Server.Client`)
|
||||
| Route | File | Render Mode | Purpose |
|
||||
|---|---|---|---|
|
||||
| `/` | `Index.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||
| `/` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor` | WebAssembly | Landing page |
|
||||
| `/sender/login` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor` | WebAssembly | Sender login |
|
||||
| `/sender` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` | WebAssembly (`prerender: false`) | Sender dashboard |
|
||||
| `/envelope/login/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` | WebAssembly | Receiver login |
|
||||
|
||||
### Sender Routes
|
||||
| Route | File | Location | Render Mode |
|
||||
### Server Pages (`EnvelopeGenerator.Server`)
|
||||
| Route | File | Render Mode | Purpose |
|
||||
|---|---|---|---|
|
||||
| `/sender/login` | `LoginSenderPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||
| `/sender` | `EnvelopeSenderPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||
|
||||
### Receiver Routes (PDF Viewers)
|
||||
| Route | File | Location | Render Mode |
|
||||
|---|---|---|---|
|
||||
| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||
| `/envelope/{EnvelopeKey}` | `EnvelopeReceiverPage.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
||||
| `/envelope/DxPdfViewer` | `EnvelopeReceiverPage_DxPdfViewer.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
||||
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeReceiverPage_DxReportViewer.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
||||
| `/envelope/Embed` | `EnvelopeReceiverPage_embed.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
||||
|
||||
**Multi-Envelope Support:** Receivers can login to multiple envelopes simultaneously (per-envelope cookie authentication).
|
||||
|
||||
**Render Mode Strategy:**
|
||||
- **Client-side Pages (WASM):** Login, Sender dashboard, Index (no DevExpress backend required)
|
||||
- **Server-side Pages (Server):** PDF viewers (DevExpress DxPdfViewer requires backend)
|
||||
| `/envelope/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` | InteractiveServer | Main receiver PDF viewer and signing page |
|
||||
| `/envelope/DxPdfViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | InteractiveServer | DevExpress PDF Viewer test page |
|
||||
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | InteractiveServer | DevExpress report-based PDF rendering |
|
||||
| `/envelope/Embed` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_embed.razor` | InteractiveServer | Embedded browser PDF view test page |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Evolution
|
||||
## Current API Location
|
||||
|
||||
### Old Architecture (Deprecated v1)
|
||||
- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit)
|
||||
- **Receiver UI:** Separate project
|
||||
- **Backend:** `EnvelopeGenerator.API`
|
||||
The active application exposes controllers from:
|
||||
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
|
||||
|
||||
### Intermediate Architecture (Deprecated v2)
|
||||
- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM)
|
||||
- **Backend:** `EnvelopeGenerator.API`
|
||||
- **Issue:** DevExpress `DxPdfViewer` displayed blank screen (no backend services in WASM)
|
||||
Current controller set includes:
|
||||
- `AnnotationController`
|
||||
- `AuthController`
|
||||
- `CacheController`
|
||||
- `ConfigController`
|
||||
- `DocumentController`
|
||||
- `EmailTemplateController`
|
||||
- `EnvelopeController`
|
||||
- `EnvelopeReceiverController`
|
||||
- `EnvelopeTypeController`
|
||||
- `HistoryController`
|
||||
- `LocalizationController`
|
||||
- `ReadOnlyController`
|
||||
- `ReceiverController`
|
||||
- `SignatureController`
|
||||
- `TfaRegistrationController`
|
||||
|
||||
### Current Architecture (Active)
|
||||
- **Frontend:** `EnvelopeGenerator.WebUI` (Blazor Auto - Server+WASM Hybrid)
|
||||
- **WebUI** (Server): Server-side components, YARP proxy, DevExpress backend
|
||||
- **WebUI.Client** (WASM): Client-side components, services, business logic
|
||||
- **Backend:** `EnvelopeGenerator.API`
|
||||
- **Libraries:** DevExpress + PDF.js
|
||||
- **PSPDFKit:** **REMOVED**
|
||||
Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||
|
||||
---
|
||||
|
||||
## Solution Structure
|
||||
## Authentication Model
|
||||
|
||||
| Project | Target | Purpose |
|
||||
|---|---|---|
|
||||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. |
|
||||
| `EnvelopeGenerator.WebUI` | net8.0 | **Blazor Auto Server Project**. YARP proxy, server-side components, DevExpress backend services. |
|
||||
| `EnvelopeGenerator.WebUI.Client` | net8.0 WASM | **Blazor Auto Client Project**. Client-side components, services, business logic. |
|
||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **DEPRECATED.** Pure Blazor WASM (migrated to WebUI). |
|
||||
| `EnvelopeGenerator.Web` | net7/8/9 | **DEPRECATED.** Legacy Razor Pages (Sender UI). No longer used. |
|
||||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. |
|
||||
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
||||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
||||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in WebUI). |
|
||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
||||
| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** |
|
||||
### Sender
|
||||
Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`.
|
||||
|
||||
Key sender endpoints:
|
||||
- `POST /api/auth?cookie=true` — login
|
||||
- `GET /api/auth/check` — current sender access check
|
||||
- `POST /api/auth/logout` — logout
|
||||
|
||||
### Receiver
|
||||
Receiver authentication is **per envelope**.
|
||||
|
||||
Key receiver endpoints used by client services:
|
||||
- `POST /api/Auth/envelope-receiver/{envelopeKey}` — submit access code
|
||||
- `GET /api/auth/check/envelope/{envelopeKey}` — check access
|
||||
- `POST /api/auth/logout/envelope/{envelopeKey}` — logout receiver for one envelope
|
||||
|
||||
Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from:
|
||||
- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern
|
||||
|
||||
### Receiver Server-Side Authorization
|
||||
`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization.
|
||||
|
||||
It uses:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||
|
||||
Behavior:
|
||||
- tries the current `HttpContext.User`
|
||||
- if needed, reads the per-envelope receiver cookie directly
|
||||
- validates the JWT with the receiver auth scheme
|
||||
- verifies the token subject matches the route envelope key
|
||||
|
||||
---
|
||||
|
||||
## Localization & Culture Management
|
||||
## Receiver Page Data Loading
|
||||
|
||||
**Current Architecture:** Blazor WebAssembly (client-side culture management)
|
||||
Main server-side page data service:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||
|
||||
### Implementation Details
|
||||
This service loads directly via MediatR and distributed cache:
|
||||
- document bytes
|
||||
- receiver envelope data
|
||||
- signature placeholders
|
||||
- cached signature data
|
||||
|
||||
**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`
|
||||
For signature placeholders, the service:
|
||||
- reads document receiver elements
|
||||
- filters them for the authenticated receiver
|
||||
- converts coordinates to `UnitOfLength.Point` before UI use
|
||||
|
||||
---
|
||||
|
||||
## Key Files & Routes
|
||||
## Receiver PDF Viewer
|
||||
|
||||
### Client-Side Pages (WebUI.Client)
|
||||
| File | Route | Purpose |
|
||||
|---|---|---|
|
||||
| `WebUI.Client/Pages/Index.razor` | `/` | Application entry point (landing page). |
|
||||
| `WebUI.Client/Pages/EnvelopeSenderPage.razor` | `/sender` | Sender dashboard (envelope list). |
|
||||
| `WebUI.Client/Pages/LoginSenderPage.razor` | `/sender/login` | Sender username/password auth. |
|
||||
| `WebUI.Client/Pages/LoginReceiverPage.razor` | `/envelope/login/{EnvelopeKey}` | Receiver access code auth. |
|
||||
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||
|
||||
### Server-Side Pages (WebUI)
|
||||
| File | Route | Purpose |
|
||||
|---|---|---|
|
||||
| `WebUI/Components/Pages/EnvelopeReceiverPage.razor` | `/envelope/{key}` | Receiver PDF viewer & signing (PDF.js). |
|
||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | `/envelope/DxPdfViewer` | DevExpress PDF Viewer (test page). |
|
||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | `/envelope/{key}/DxReportViewer` | DevExpress Report Viewer. |
|
||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_embed.razor` | `/envelope/Embed` | Embedded PDF viewer (iframe). |
|
||||
Current receiver viewer characteristics:
|
||||
- route: `/envelope/{EnvelopeKey}`
|
||||
- render mode: `InteractiveServer`
|
||||
- PDF rendering: `PDF.js`
|
||||
- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset
|
||||
- signature popup: `DxPopup`
|
||||
- thumbnail sidebar: resizable and stored in `localStorage`
|
||||
|
||||
### Services & Assets
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `WebUI.Client/Services/AuthService.cs` | Receiver + Sender authentication. |
|
||||
| `WebUI.Client/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). |
|
||||
| `WebUI.Client/Services/DocumentService.cs` | PDF document retrieval. |
|
||||
| `WebUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). |
|
||||
| `WebUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). |
|
||||
| `WebUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. |
|
||||
| `API/Controllers/CacheController.cs` | Signature cache endpoints. |
|
||||
### JS Assets
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
|
||||
|
||||
### CSS
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
|
||||
|
||||
### PDF.js CDN
|
||||
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css`
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System — CRITICAL
|
||||
## Signature Workflow
|
||||
|
||||
**Database Format:** INCHES (GdPicture14 native)
|
||||
**Origin:** Top-left corner
|
||||
**Axes:** X right, Y down
|
||||
Receiver signatures are handled as a **viewer overlay workflow**.
|
||||
|
||||
### Conversion Formulas
|
||||
### Current behavior
|
||||
1. Server-side authorization validates receiver access.
|
||||
2. The page loads document bytes, receiver data, signature placeholders, and cached signature state.
|
||||
3. If no cached signature exists, the signature popup opens automatically.
|
||||
4. Receiver creates signature using one of three tabs:
|
||||
- draw
|
||||
- text
|
||||
- image
|
||||
5. Required metadata:
|
||||
- full name
|
||||
- place
|
||||
6. Optional metadata:
|
||||
- position
|
||||
7. Clicking a signature placeholder applies the signature as a client-side overlay in the PDF viewer.
|
||||
|
||||
| From INCHES to | Formula | Example |
|
||||
|---|---|---|
|
||||
| **DevExpress DX** | `x_DX = x_inches * 100` | 1.5" ? 150 DX |
|
||||
| **PDF Points** | `x_pt = x_inches * 72` | 1.5" ? 108 pt |
|
||||
| **PDF.js Pixels** | Normalize ? scale | `(x_inches / pageWidth) * canvasWidth * scale` |
|
||||
### Important note
|
||||
Although `itext` is referenced by the server project, the current receiver page signing flow is **not PDF stamping-based**. The active receiver UI uses client-side overlay behavior in the viewer.
|
||||
|
||||
**A4 Dimensions:**
|
||||
- Width: 8.27" = 595pt = 827 DX
|
||||
- Height: 11.69" = 842pt = 1169 DX
|
||||
|
||||
### Unit Systems
|
||||
|
||||
| System | Unit | Origin | Y-Axis |
|
||||
|---|---|---|---|
|
||||
| **Database (GdPicture14)** | Inches | Top-left | Down |
|
||||
| PDF.js | Pixels | Top-left | Down |
|
||||
| iText7 PDF | Points (1/72") | **Bottom-left** | **Up** (flip required) |
|
||||
| ~~PSPDFKit~~ | ~~Points~~ | ~~Top-left~~ | **REMOVED** |
|
||||
|
||||
---
|
||||
|
||||
## EnvelopeReceiver — PDF.js Viewer & Signing
|
||||
|
||||
**Route:** `/envelope/{EnvelopeKey}`
|
||||
**Tech:** PDF.js 3.11.174 + Blazor Server (`@rendermode InteractiveServer`) + configurable quality
|
||||
**File:** `WebUI/Components/Pages/EnvelopeReceiverPage.razor`
|
||||
|
||||
### Key Features
|
||||
1. HiDPI/Retina support (4x quality)
|
||||
2. Configurable quality (`appsettings.json`)
|
||||
3. Unlimited zoom (50%-300%)
|
||||
4. Ctrl+Wheel global zoom
|
||||
5. Resizable thumbnail sidebar (150-400px, localStorage)
|
||||
6. Responsive (desktop/mobile)
|
||||
|
||||
### Configuration
|
||||
**File:** `WebUI/wwwroot/appsettings.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"PdfViewer": {
|
||||
"ThumbnailBaseScale": 0.75,
|
||||
"ThumbnailEnableHiDPI": true,
|
||||
"MainCanvasEnableHiDPI": true,
|
||||
"ZoomStepPercentage": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript API
|
||||
**File:** `WebUI/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 Workflow — EnvelopeReceiver
|
||||
|
||||
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
|
||||
|
||||
### Workflow Steps
|
||||
|
||||
1. **Page Load:**
|
||||
- Check `SignatureCacheService` for cached signature
|
||||
- If cached ? skip popup, load signature
|
||||
- If not ? show automatic popup (mandatory)
|
||||
|
||||
2. **Signature Popup (DxPopup):**
|
||||
- **Cannot close** (no X, no ESC, no outside-click)
|
||||
- **3 Tabs:** Draw (canvas) / Text (font select) / Image (upload)
|
||||
- **Required:** Full name, Place
|
||||
- **Optional:** Position
|
||||
- **Save ?** Store in `_capturedSignature`, cache via API
|
||||
|
||||
3. **Signature Buttons:**
|
||||
- Render purple "Unterschreiben" buttons at signature field positions
|
||||
- Coordinates: INCHES ? POINTS ? Pixels (scaled)
|
||||
- File: `pdf-viewer.js` ? `renderSignatureButtons()`
|
||||
|
||||
4. **Apply Signature (Click "Unterschreiben"):**
|
||||
- JS: Remove button, create HTML overlay
|
||||
- Format: Image + separator + text (Name, Position, Place, Date)
|
||||
- **NOT stamped on PDF bytes** (visual overlay only)
|
||||
|
||||
5. **Re-rendering:**
|
||||
- Zoom/Page change ? recalculate button positions
|
||||
- Session state: `_capturedSignature` (lost on refresh)
|
||||
|
||||
### Data Model
|
||||
**File:** `WebUI.Client/Models/SignatureCaptureDto.cs`
|
||||
### Signature DTO
|
||||
`EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs`
|
||||
|
||||
```csharp
|
||||
public sealed record SignatureCaptureDto {
|
||||
public required string DataUrl { get; init; } // base64 PNG
|
||||
public required string DataUrl { get; init; }
|
||||
public required string FullName { get; init; }
|
||||
public string Position { get; init; } = ""; // Optional
|
||||
public string Position { get; init; } = "";
|
||||
public required string Place { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signature Caching
|
||||
## Signature Cache
|
||||
|
||||
**Purpose:** Persist signature across page refreshes (distributed cache: Redis/SQL)
|
||||
### Active cache model
|
||||
The current receiver page cache flow is handled directly in the server project through:
|
||||
- `EnvelopeReceiverPageDataService`
|
||||
- `IDistributedCache`
|
||||
- SQL Server distributed cache configuration from `Program.cs`
|
||||
|
||||
### API Endpoints
|
||||
**Controller:** `API/Controllers/CacheController.cs`
|
||||
### Cache key format
|
||||
Current server-side key prefix:
|
||||
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
|
||||
|
||||
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
|
||||
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
|
||||
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
|
||||
This is different from an envelope-key-only cache convention.
|
||||
|
||||
**Cache Key Format:**
|
||||
```
|
||||
signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
|
||||
```
|
||||
### Config
|
||||
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
|
||||
- section name: `Cache`
|
||||
- option: `SignatureCacheExpiration`
|
||||
|
||||
**Configuration:** `appsettings.json`
|
||||
```json
|
||||
{
|
||||
"Cache": {
|
||||
"SignatureCacheExpiration": null // or "02:00:00" for 2h
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service
|
||||
**File:** `WebUI.Client/Services/SignatureCacheService.cs`
|
||||
|
||||
```csharp
|
||||
public class SignatureCacheService {
|
||||
Task SaveSignatureAsync(string envelopeKey, SignatureCaptureDto signature);
|
||||
Task<SignatureCaptureDto?> GetSignatureAsync(string envelopeKey);
|
||||
Task DeleteSignatureAsync(string envelopeKey);
|
||||
}
|
||||
```
|
||||
|
||||
**Error Handling:** Fire-and-forget saves, graceful degradation on load failure.
|
||||
### Related controller
|
||||
A cache API controller also exists in:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
|
||||
|
||||
---
|
||||
|
||||
## Sender Login
|
||||
## Sender Dashboard
|
||||
|
||||
**Route:** `/sender/login`
|
||||
**File:** `WebUI.Client/Pages/LoginSenderPage.razor`
|
||||
**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme
|
||||
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
|
||||
|
||||
### AuthService Extension
|
||||
**File:** `WebUI.Client/Services/AuthService.cs`
|
||||
Current behavior:
|
||||
- checks sender access through `AuthService.CheckSenderAccessAsync()`
|
||||
- redirects to `/sender/login` when unauthorized
|
||||
- loads envelope list through client `EnvelopeService`
|
||||
- separates envelopes into active/completed tabs
|
||||
- uses `DevExpress DxGrid`
|
||||
|
||||
```csharp
|
||||
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
||||
|
||||
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password) {
|
||||
var response = await http.PostAsJsonAsync(
|
||||
$"{_api.BaseUrl}/api/auth?cookie=true",
|
||||
new { username, password });
|
||||
|
||||
return response.StatusCode switch {
|
||||
HttpStatusCode.OK => SenderLoginResult.Success,
|
||||
HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
|
||||
_ => SenderLoginResult.Error
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### API Integration
|
||||
**Endpoint:** `POST /api/auth?cookie=true`
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "username": "TekH", "password": "***" }
|
||||
```
|
||||
|
||||
**Response:**
|
||||
- `200 OK` ? Cookie set, redirect to `/sender`
|
||||
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
|
||||
- Other ? Show error: "Serverfehler"
|
||||
|
||||
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
|
||||
|
||||
### UI Flow
|
||||
1. User enters username + password
|
||||
2. Click "Anmelden" or press Enter
|
||||
3. Call `AuthService.LoginSenderAsync()`
|
||||
4. Success ? `Navigation.NavigateTo("/sender", forceLoad: true)`
|
||||
5. Error ? Display alert
|
||||
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
|
||||
|
||||
---
|
||||
|
||||
## Receiver Login
|
||||
## Localization
|
||||
|
||||
**Route:** `/envelope/login/{EnvelopeKey}`
|
||||
**File:** `WebUI.Client/Pages/LoginReceiverPage.razor`
|
||||
Current server host localization setup in `Program.cs`:
|
||||
- supported cultures: `de-DE`, `en-US`
|
||||
- request localization middleware is enabled
|
||||
- `QueryStringRequestCultureProvider` is added
|
||||
- cookie-based localization services are registered via `AddCookieBasedLocalizer()`
|
||||
|
||||
**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session.
|
||||
|
||||
### AuthService Method
|
||||
```csharp
|
||||
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
||||
|
||||
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, string accessCode) {
|
||||
var form = new MultipartFormDataContent();
|
||||
form.Add(new StringContent(accessCode), "AccessCode");
|
||||
|
||||
var response = await http.PostAsync(
|
||||
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(key)}", form);
|
||||
|
||||
return response.StatusCode switch {
|
||||
HttpStatusCode.OK => EnvelopeLoginResult.Success,
|
||||
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
|
||||
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
|
||||
_ => EnvelopeLoginResult.Error
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Success:** Redirect to `/envelope/{key}`
|
||||
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
|
||||
|
||||
---
|
||||
|
||||
## NuGet Packages (WebUI.Client)
|
||||
## Coordinate System
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---|---|---|
|
||||
| `DevExpress.Blazor.*` | 25.2.3 | UI components (grids, popups, etc.) |
|
||||
| `SkiaSharp.*` | 3.119.1 | WASM rendering |
|
||||
| ~~`itext`~~ | ~~8.0.5~~ | **NOT USED** (GPL license) |
|
||||
### Source data
|
||||
Database signature coordinates are still based on:
|
||||
- **unit:** inches
|
||||
- **origin:** top-left
|
||||
- **axes:** X right, Y down
|
||||
|
||||
**External CDN:**
|
||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||
### Relevant conversions
|
||||
- inches -> PDF points: `x_pt = x_inches * 72`
|
||||
- inches -> DevExpress DX units: `x_dx = x_inches * 100`
|
||||
|
||||
### Current receiver page behavior
|
||||
The server page data service converts signature placeholders to **points** before sending them into the viewer workflow.
|
||||
|
||||
### Unit systems to keep in mind
|
||||
| System | Unit | Origin | Y-axis |
|
||||
|---|---|---|---|
|
||||
| Database | Inches | Top-left | Down |
|
||||
| PDF.js display | Pixels | Top-left | Down |
|
||||
| PDF points | Points | Depends on PDF model | Depends on consumer |
|
||||
| DevExpress DX | 1/100 inch style coordinates | Top-left-oriented usage in this app | Down-oriented usage |
|
||||
|
||||
---
|
||||
|
||||
## Mistakes History — Do NOT Repeat
|
||||
## Key Services and Files
|
||||
|
||||
| Mistake | Why Wrong |
|
||||
|---|---|
|
||||
| Using iText7 in EnvelopeReceiver | GPL license issue. Use overlay system instead. |
|
||||
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
|
||||
| Hardcoded quality values in PDF.js | Use `appsettings.json` for configurability. |
|
||||
| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. |
|
||||
| Over-designed UI (gradients/badges) | User prefers simple text labels. |
|
||||
| Ignoring "revert" instructions | Revert HTML structure, not just CSS. |
|
||||
| `BottomMarginBand` for signatures | Repeats on every page. Use DetailBand. |
|
||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand. Calculate per-page. |
|
||||
### Client services
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs`
|
||||
|
||||
### Server services
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||
|
||||
### Server config and host files
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||
|
||||
---
|
||||
|
||||
## Development Notes
|
||||
## Working Rules for This Workspace
|
||||
|
||||
### 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
|
||||
|
||||
### Legacy Projects (VB.NET)
|
||||
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
|
||||
|
||||
### Signature Coordinate Evidence
|
||||
**File:** `EnvelopeGenerator.Form/frmFieldEditor.vb` (VB.NET)
|
||||
|
||||
```vb
|
||||
Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches
|
||||
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches
|
||||
|
||||
Sub LoadAnnotation(pElement As Signature, ...)
|
||||
oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment
|
||||
oAnnotation.Top = CSng(pElement.Y)
|
||||
End Sub
|
||||
```
|
||||
|
||||
Proves database uses INCHES natively.
|
||||
- Treat `EnvelopeGenerator.Server` as the active main application host.
|
||||
- Treat `EnvelopeGenerator.Server.Client` as the active client UI project.
|
||||
- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references.
|
||||
- Do not use `EnvelopeGenerator.Web` or `EnvelopeGenerator.ReceiverUI` as the primary implementation target unless explicitly asked.
|
||||
- Do not modify the legacy VB.NET projects unless explicitly requested.
|
||||
- For receiver PDF/signature work, prefer the current `PDF.js`-based flow in `EnvelopeReceiverPage.razor`.
|
||||
- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### When working with coordinates:
|
||||
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:
|
||||
1. Check `Mistakes History` first
|
||||
2. Prefer simplicity over complexity
|
||||
3. Use `appsettings.json` for configuration
|
||||
4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry)
|
||||
5. **Unified frontend:** WebUI serves both Senders and Receivers
|
||||
6. **Render mode:** Client-side (WASM) for login/dashboard, Server-side for PDF viewers
|
||||
|
||||
### When debugging:
|
||||
1. **Coordinates:** Always check unit system (inches/points/pixels)
|
||||
2. **Authentication:** Check cookie name/domain/SameSite
|
||||
3. **Cache:** Check Redis/SQL connection + key format
|
||||
4. **Frontend confusion:** Only use WebUI (ReceiverUI/Web are deprecated)
|
||||
5. **Blank DxPdfViewer:** Ensure page has `@rendermode InteractiveServer`
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-01-27 (ReceiverUI ? WebUI migration complete)
|
||||
**Last Updated:** 2026-06-29
|
||||
|
||||
@@ -23,7 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B29
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
||||
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
||||
OPEN_SSR_TASK.md = OPEN_SSR_TASK.md
|
||||
RECEIVER_PDF_VIEWER_CONTEXT.md = RECEIVER_PDF_VIEWER_CONTEXT.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
||||
|
||||
553
OPEN_SSR_TASK.md
553
OPEN_SSR_TASK.md
@@ -1,553 +0,0 @@
|
||||
# SSR Authentication Migration — Implementation Notes
|
||||
|
||||
## Overview
|
||||
Migration from WASM client-side authentication to SSR (Server-Side Rendering) authentication for `EnvelopeReceiverPage.razor` to fix authentication issues in Blazor InteractiveServer mode.
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
### Issue
|
||||
`EnvelopeReceiverPage.razor` uses `@rendermode InteractiveServer` but was calling **WASM client service** `AuthService.CheckEnvelopeAccessAsync()`:
|
||||
|
||||
```razor
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
```
|
||||
|
||||
**Why This Failed:**
|
||||
- `AuthService` is a **WASM client service** that uses `IHttpClientFactory`
|
||||
- In SSR context, `HttpContext` is required to configure the base address
|
||||
- `CheckEnvelopeAccessAsync()` makes an HTTP request to `/api/auth/check/envelope/{key}`
|
||||
- This request **goes to itself** (server calling its own endpoint), causing issues
|
||||
- Returns `false` even when user is authenticated
|
||||
|
||||
---
|
||||
|
||||
## Solution Architecture
|
||||
|
||||
### Created New SSR Authentication Service
|
||||
|
||||
**Files Created:**
|
||||
1. `EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs` (Interface)
|
||||
2. `EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs` (Implementation)
|
||||
|
||||
**Purpose:** Direct `HttpContext.User` validation without HTTP requests
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. IEnvelopeAuthService Interface
|
||||
|
||||
**Location:** `EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||
|
||||
```csharp
|
||||
namespace EnvelopeGenerator.Server.Services;
|
||||
|
||||
public interface IEnvelopeAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the current user is authenticated for the given envelope key.
|
||||
/// Validates both that the user is authenticated AND that the envelope key matches their claims.
|
||||
/// </summary>
|
||||
bool IsAuthenticated(string envelopeKey);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope key from the current user's claims (NameIdentifier or "sub" claim).
|
||||
/// </summary>
|
||||
string? GetAuthenticatedEnvelopeKey();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current HttpContext user principal.
|
||||
/// </summary>
|
||||
ClaimsPrincipal? GetCurrentUser();
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods:**
|
||||
- `IsAuthenticated(string envelopeKey)`: Validates user auth + envelope key match
|
||||
- `GetAuthenticatedEnvelopeKey()`: Extracts envelope key from claims
|
||||
- `GetCurrentUser()`: Returns `ClaimsPrincipal` for advanced scenarios
|
||||
|
||||
---
|
||||
|
||||
### 2. EnvelopeAuthService Implementation
|
||||
|
||||
**Location:** `EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||
|
||||
**Dependencies:**
|
||||
- `IHttpContextAccessor`: Access current HTTP context
|
||||
- `ILogger<EnvelopeAuthService>`: Structured logging
|
||||
|
||||
**Logic:**
|
||||
```csharp
|
||||
public bool IsAuthenticated(string envelopeKey)
|
||||
{
|
||||
// 1. Validate envelope key parameter
|
||||
if (string.IsNullOrWhiteSpace(envelopeKey))
|
||||
return false;
|
||||
|
||||
// 2. Get HttpContext
|
||||
var context = _httpContextAccessor.HttpContext;
|
||||
|
||||
// 3. Check if user is authenticated
|
||||
if (context?.User?.Identity?.IsAuthenticated != true)
|
||||
return false;
|
||||
|
||||
// 4. Extract envelope key from claims
|
||||
var sub = GetEnvelopeKeyFromClaims(context.User);
|
||||
|
||||
// 5. Verify match
|
||||
return sub == envelopeKey;
|
||||
}
|
||||
|
||||
private string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user)
|
||||
{
|
||||
// Try standard claim first
|
||||
var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
// Fallback to JWT "sub" claim
|
||||
if (string.IsNullOrWhiteSpace(sub))
|
||||
sub = user.FindFirst("sub")?.Value;
|
||||
|
||||
return sub;
|
||||
}
|
||||
```
|
||||
|
||||
**Claim Priority:**
|
||||
1. `ClaimTypes.NameIdentifier` (standard .NET claim)
|
||||
2. `"sub"` (JWT standard claim)
|
||||
|
||||
---
|
||||
|
||||
### 3. Service Registration
|
||||
|
||||
**Location:** `EnvelopeGenerator.Server/Program.cs`
|
||||
|
||||
**Added:**
|
||||
```csharp
|
||||
// SSR Authentication Service (for Envelope Receiver pages)
|
||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.IEnvelopeAuthService,
|
||||
EnvelopeGenerator.Server.Services.EnvelopeAuthService>();
|
||||
```
|
||||
|
||||
**Lifetime:** `Scoped` (per-request, matches `IHttpContextAccessor`)
|
||||
|
||||
---
|
||||
|
||||
### 4. EnvelopeReceiverPage.razor Changes
|
||||
|
||||
**Changes Made (REVERTED - To Be Re-Applied):**
|
||||
|
||||
#### 4.1 Using Statements
|
||||
```razor
|
||||
@using EnvelopeGenerator.Server.Services
|
||||
```
|
||||
|
||||
#### 4.2 Dependency Injection
|
||||
**Old:**
|
||||
```razor
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
```
|
||||
|
||||
**New:**
|
||||
```razor
|
||||
@inject IEnvelopeAuthService EnvelopeAuth
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
```
|
||||
|
||||
#### 4.3 Authentication Check in `OnInitializedAsync()`
|
||||
**Old:**
|
||||
```csharp
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
if (!hasAccess) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**New:**
|
||||
```csharp
|
||||
// ? SSR Authentication check via service
|
||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ? Synchronous (no HTTP overhead)
|
||||
- ? Direct `HttpContext.User` access
|
||||
- ? No self-referencing HTTP calls
|
||||
- ? Works in SSR context
|
||||
|
||||
#### 4.4 Logout Method
|
||||
**Old:**
|
||||
```csharp
|
||||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
||||
```
|
||||
|
||||
**New:**
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
// ? SSR: Direct HTTP call instead of WASM client service
|
||||
using var http = HttpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
await http.PostAsync($"/api/auth/logout/envelope/{Uri.EscapeDataString(EnvelopeKey)}", null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Logout failed for envelope {EnvelopeKey}", EnvelopeKey);
|
||||
}
|
||||
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||
```
|
||||
|
||||
**Why Changed:**
|
||||
- WASM `AuthService.LogoutEnvelopeReceiverAsync()` doesn't work in SSR
|
||||
- Use named HttpClient `"EnvelopeGenerator.Server"` (configured in `Program.cs`)
|
||||
- Graceful error handling (logout errors shouldn't block redirect)
|
||||
|
||||
---
|
||||
|
||||
## Remaining Tasks
|
||||
|
||||
### ? Completed
|
||||
1. ? Created `IEnvelopeAuthService` interface
|
||||
2. ? Implemented `EnvelopeAuthService` with `HttpContext` access
|
||||
3. ? Registered service in `Program.cs`
|
||||
4. ?? **REVERTED** `EnvelopeReceiverPage.razor` changes (merge conflict)
|
||||
|
||||
### ? TODO (Next Agent)
|
||||
|
||||
#### 1. Re-apply EnvelopeReceiverPage.razor Changes
|
||||
**File:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||
|
||||
**Steps:**
|
||||
1. Add using statement:
|
||||
```razor
|
||||
@using EnvelopeGenerator.Server.Services
|
||||
```
|
||||
|
||||
2. Replace injection:
|
||||
```razor
|
||||
@inject IEnvelopeAuthService EnvelopeAuth
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
```
|
||||
|
||||
Remove:
|
||||
```razor
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
```
|
||||
|
||||
3. Update `OnInitializedAsync()` authentication check:
|
||||
```csharp
|
||||
// Replace this:
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
if (!hasAccess) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
|
||||
// With this:
|
||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
4. Update `LogoutAsync()` method:
|
||||
```csharp
|
||||
async Task LogoutAsync() {
|
||||
if (string.IsNullOrWhiteSpace(EnvelopeKey) || _isLoggingOut) return;
|
||||
_isLoggingOut = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
// ? SSR: Direct HTTP call instead of WASM client service
|
||||
using var http = HttpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
await http.PostAsync($"/api/auth/logout/envelope/{Uri.EscapeDataString(EnvelopeKey)}", null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Logout failed for envelope {EnvelopeKey}", EnvelopeKey);
|
||||
}
|
||||
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Test Authentication Flow
|
||||
**Scenarios:**
|
||||
- ? Valid cookie ? Page loads
|
||||
- ? Invalid cookie ? Redirect to login
|
||||
- ? No cookie ? Redirect to login
|
||||
- ? Envelope key mismatch ? Redirect to login
|
||||
- ? Logout ? Cookie cleared, redirect to login
|
||||
|
||||
#### 3. Remove WASM Client Services from SSR Pages
|
||||
**Optional Cleanup:**
|
||||
- Review other SSR pages (`EnvelopeReceiverPage_DxPdfViewer.razor`, etc.)
|
||||
- Replace WASM client services with SSR equivalents where applicable
|
||||
- Document which services are WASM-only vs SSR-compatible
|
||||
|
||||
---
|
||||
|
||||
## Authentication Flow Comparison
|
||||
|
||||
### ? Old Flow (WASM Client Service in SSR)
|
||||
```
|
||||
EnvelopeReceiverPage (@rendermode InteractiveServer)
|
||||
?
|
||||
AuthService.CheckEnvelopeAccessAsync() (WASM client)
|
||||
?
|
||||
IHttpClientFactory.CreateClient("EnvelopeGenerator.Server")
|
||||
?
|
||||
GET /api/auth/check/envelope/{key}
|
||||
?
|
||||
[SELF-REFERENCING REQUEST - FAILS]
|
||||
?
|
||||
Returns false even when authenticated
|
||||
```
|
||||
|
||||
### ? New Flow (SSR Service)
|
||||
```
|
||||
EnvelopeReceiverPage (@rendermode InteractiveServer)
|
||||
?
|
||||
IEnvelopeAuthService.IsAuthenticated(envelopeKey)
|
||||
?
|
||||
IHttpContextAccessor.HttpContext.User (Direct access)
|
||||
?
|
||||
ClaimsPrincipal.FindFirst("sub" or NameIdentifier)
|
||||
?
|
||||
Compare with envelopeKey
|
||||
?
|
||||
Return true/false (synchronous, no HTTP)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Decisions
|
||||
|
||||
### Why Not Use `[Authorize]` Attribute?
|
||||
- Blazor SSR components **don't support** `[Authorize]` at component level
|
||||
- Would require `<AuthorizeView>` component (less clean)
|
||||
- Custom service provides more control + logging
|
||||
|
||||
### Why Scoped Lifetime?
|
||||
- `IHttpContextAccessor` is scoped (per-request)
|
||||
- `EnvelopeAuthService` depends on `IHttpContextAccessor`
|
||||
- Scoped ensures same `HttpContext` throughout request
|
||||
|
||||
### Why Two Claims (`NameIdentifier` + `"sub"`)?
|
||||
- **`NameIdentifier`**: Standard .NET claim type
|
||||
- **`"sub"`**: JWT standard claim
|
||||
- Fallback ensures compatibility with different token formats
|
||||
|
||||
---
|
||||
|
||||
## Logging & Debugging
|
||||
|
||||
### Log Levels
|
||||
- **Debug:** Successful authentication
|
||||
- **Warning:** Null envelope key, key mismatch
|
||||
- **Error:** (Reserved for future exceptions)
|
||||
|
||||
### Sample Logs
|
||||
```
|
||||
[Debug] User authenticated for envelope 517bb9c5-6082-4e61-aaa5-9846386e67ee
|
||||
[Warning] Envelope key mismatch: Expected abc123, Got 517bb9c5-6082-4e61-aaa5-9846386e67ee
|
||||
[Warning] IsAuthenticated called with null or empty envelope key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Unit Tests (TODO)
|
||||
```csharp
|
||||
// EnvelopeGenerator.Tests/Services/EnvelopeAuthServiceTests.cs
|
||||
[Fact]
|
||||
public void IsAuthenticated_ValidUser_ReturnsTrue() { ... }
|
||||
|
||||
[Fact]
|
||||
public void IsAuthenticated_InvalidKey_ReturnsFalse() { ... }
|
||||
|
||||
[Fact]
|
||||
public void IsAuthenticated_UnauthenticatedUser_ReturnsFalse() { ... }
|
||||
|
||||
[Fact]
|
||||
public void GetAuthenticatedEnvelopeKey_ValidUser_ReturnsKey() { ... }
|
||||
```
|
||||
|
||||
### Integration Tests (Manual)
|
||||
1. ? Login with valid access code ? Cookie set
|
||||
2. ? Navigate to `/envelope/{key}` ? Page loads
|
||||
3. ? Logout ? Cookie cleared, redirect
|
||||
4. ? Try accessing `/envelope/{key}` without cookie ? Redirect to login
|
||||
5. ? Try accessing `/envelope/{wrongKey}` with valid cookie ? Redirect to login
|
||||
|
||||
---
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
- [x] Create `IEnvelopeAuthService` interface
|
||||
- [x] Implement `EnvelopeAuthService`
|
||||
- [x] Register service in `Program.cs`
|
||||
- [ ] **Re-apply** `EnvelopeReceiverPage.razor` changes (after merge)
|
||||
- [ ] Test authentication flow
|
||||
- [ ] Add unit tests
|
||||
- [ ] Update other SSR pages (if needed)
|
||||
- [ ] Document in `COPILOT_CONTEXT.md`
|
||||
|
||||
---
|
||||
|
||||
## Documentation Updates Needed
|
||||
|
||||
### COPILOT_CONTEXT.md
|
||||
|
||||
**Add Section:**
|
||||
```markdown
|
||||
## SSR Authentication Service
|
||||
|
||||
**Purpose:** Server-side authentication for Blazor InteractiveServer pages.
|
||||
|
||||
**Location:** `EnvelopeGenerator.Server/Services/`
|
||||
|
||||
**Service:** `IEnvelopeAuthService` / `EnvelopeAuthService`
|
||||
|
||||
**Usage:**
|
||||
```razor
|
||||
@inject IEnvelopeAuthService EnvelopeAuth
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why Not Use WASM Client Services in SSR?**
|
||||
- WASM client services use `IHttpClientFactory` with base address configuration
|
||||
- SSR context requires `HttpContext` to configure base address
|
||||
- Calling API endpoints from server-side component creates self-referencing requests
|
||||
- Use `IEnvelopeAuthService` for direct `HttpContext.User` access instead
|
||||
|
||||
**Authentication Flow:**
|
||||
1. JWT token stored in per-envelope cookie (`AuthTokenSignFLOWReceiver.{envelopeKey}`)
|
||||
2. JWT middleware validates token, sets `HttpContext.User`
|
||||
3. `EnvelopeAuthService` checks `ClaimsPrincipal.FindFirst("sub")` or `NameIdentifier`
|
||||
4. Compares claim value with route parameter `{EnvelopeKey}`
|
||||
|
||||
**Claim Priority:**
|
||||
1. `ClaimTypes.NameIdentifier` (standard .NET)
|
||||
2. `"sub"` (JWT standard)
|
||||
|
||||
**Service Lifetime:** Scoped (per-request)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### ? Don't Do This
|
||||
```csharp
|
||||
// SSR page using WASM client service
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
```
|
||||
|
||||
**Why Wrong:**
|
||||
- Creates self-referencing HTTP request
|
||||
- WASM client service doesn't work in SSR context
|
||||
- Always returns `false` even when authenticated
|
||||
|
||||
### ? Do This Instead
|
||||
```csharp
|
||||
// SSR page using SSR authentication service
|
||||
@inject IEnvelopeAuthService EnvelopeAuth
|
||||
|
||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Why Correct:**
|
||||
- Direct `HttpContext.User` access
|
||||
- Synchronous (no HTTP overhead)
|
||||
- Works in SSR context
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### Related Files
|
||||
- `EnvelopeGenerator.Server/Program.cs` (Service registration, JWT middleware)
|
||||
- `EnvelopeGenerator.Server.Client/Services/AuthService.cs` (WASM client version)
|
||||
- `EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` (WASM login page)
|
||||
- `EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` (SSR viewer page)
|
||||
|
||||
### JWT Configuration
|
||||
**File:** `EnvelopeGenerator.Server/Program.cs`
|
||||
|
||||
```csharp
|
||||
.AddJwtBearer(AuthScheme.Receiver, opt =>
|
||||
{
|
||||
opt.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var envelopeKey = context.Request.Path.Value?.Split('/').LastOrDefault();
|
||||
if (envelopeKey is not null)
|
||||
{
|
||||
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
||||
if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken))
|
||||
context.Token = cookieToken;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnTokenValidated = context =>
|
||||
{
|
||||
var envelopeKey = context.Request.Path.Value?.Split('/').LastOrDefault();
|
||||
var sub = context.Principal?.FindFirst("sub")?.Value;
|
||||
|
||||
if (envelopeKey is null || sub != envelopeKey)
|
||||
context.Fail("Envelope key mismatch");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**What Was Done:**
|
||||
1. Created SSR authentication service (`IEnvelopeAuthService` / `EnvelopeAuthService`)
|
||||
2. Registered service in DI container
|
||||
3. Updated `EnvelopeReceiverPage.razor` (REVERTED due to merge)
|
||||
|
||||
**What's Left:**
|
||||
1. **Re-apply** `EnvelopeReceiverPage.razor` changes after merge
|
||||
2. Test authentication flow
|
||||
3. Add unit tests
|
||||
4. Update documentation
|
||||
|
||||
**Key Insight:**
|
||||
- **WASM client services ? SSR server services**
|
||||
- Use `IHttpContextAccessor` for direct `HttpContext.User` access in SSR
|
||||
- Avoid HTTP requests from server-side components to own endpoints
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-01-27
|
||||
**Status:** ?? Partial (Service created, page changes reverted for merge)
|
||||
**Next Agent:** Re-apply `EnvelopeReceiverPage.razor` changes + testing
|
||||
1113
RECEIVER_PDF_VIEWER_CONTEXT.md
Normal file
1113
RECEIVER_PDF_VIEWER_CONTEXT.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user