Compare commits
7 Commits
b6ec5307b6
...
71e375d6ea
| Author | SHA1 | Date | |
|---|---|---|---|
| 71e375d6ea | |||
| 05f64e2b61 | |||
| ed17852542 | |||
| 9947774ba8 | |||
| c6c1decd2a | |||
| 0fdaa1a38d | |||
| 5d66de9f32 |
@@ -15,13 +15,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Inject] HttpClient Http { get; set; }
|
[Inject] IHttpClientFactory HttpClientFactory { get; set; } = default!;
|
||||||
|
|
||||||
List<string> RequiredFonts = new() {
|
List<string> RequiredFonts = new() {
|
||||||
"opensans.ttf"
|
"opensans.ttf"
|
||||||
};
|
};
|
||||||
|
|
||||||
protected async override Task OnInitializedAsync() {
|
protected async override Task OnInitializedAsync() {
|
||||||
await FontLoader.LoadFonts(Http, RequiredFonts);
|
await FontLoader.LoadFonts(HttpClientFactory, RequiredFonts);
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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="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">
|
<div class="mb-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" viewBox="0 0 16 16">
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="mb-0 fw-semibold">Dokument öffnen</h5>
|
<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.
|
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
|
||||||
</p>
|
</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">
|
<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">
|
<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>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Dokument nicht gefunden.</strong><br />
|
<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>
|
<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>
|
||||||
</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">
|
<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">
|
<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>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Ungültiger Zugangscode.</strong><br />
|
<strong>Ungültiger Zugangscode.</strong><br />
|
||||||
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
<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="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="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>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Serverfehler.</strong><br />
|
<strong>Serverfehler.</strong><br />
|
||||||
@@ -66,7 +71,7 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text bg-light border-end-0">
|
<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">
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<input id="login-access-code"
|
<input id="login-access-code"
|
||||||
@@ -83,17 +88,20 @@
|
|||||||
style="border-left: none;"
|
style="border-left: none;"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@onclick="() => ShowCode = !ShowCode">
|
@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">
|
<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="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="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 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 fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z" />
|
||||||
</svg>
|
</svg>
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
<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="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="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>
|
</svg>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
@@ -104,12 +112,15 @@
|
|||||||
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
||||||
@onclick="SubmitAsync"
|
@onclick="SubmitAsync"
|
||||||
disabled="@(IsLoading || string.IsNullOrWhiteSpace(AccessCode))">
|
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 class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
<span>Überprüfen …</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">
|
<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>
|
</svg>
|
||||||
<span>Dokument öffnen</span>
|
<span>Dokument öffnen</span>
|
||||||
}
|
}
|
||||||
@@ -132,12 +143,14 @@
|
|||||||
bool IsLoading;
|
bool IsLoading;
|
||||||
EnvelopeLoginResult? LoginResult;
|
EnvelopeLoginResult? LoginResult;
|
||||||
|
|
||||||
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
|
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e)
|
||||||
|
{
|
||||||
if (e.Key == "Enter")
|
if (e.Key == "Enter")
|
||||||
await SubmitAsync();
|
await SubmitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task SubmitAsync() {
|
async Task SubmitAsync()
|
||||||
|
{
|
||||||
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
|
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
|
||||||
|
|
||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
@@ -146,7 +159,8 @@
|
|||||||
|
|
||||||
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
||||||
|
|
||||||
if (result == EnvelopeLoginResult.Success) {
|
if (result == EnvelopeLoginResult.Success)
|
||||||
|
{
|
||||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -156,3 +170,4 @@
|
|||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,19 @@ using DevExpress.XtraReports.Services;
|
|||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
// HTTP Client (uses Server's YARP proxy)
|
// Named HttpClient for API calls (both for services and DevExpress components)
|
||||||
builder.Services.AddScoped(sp => new HttpClient {
|
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)
|
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configuration Options
|
// Configuration Options
|
||||||
builder.Services.Configure<ApiOptions>(opts =>
|
|
||||||
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
|
|
||||||
builder.Services.Configure<PdfViewerOptions>(opts =>
|
builder.Services.Configure<PdfViewerOptions>(opts =>
|
||||||
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
||||||
|
|
||||||
@@ -51,5 +56,5 @@ builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
|
|||||||
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
||||||
|
|
||||||
var host = builder.Build();
|
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();
|
await host.RunAsync();
|
||||||
|
|||||||
@@ -2,21 +2,15 @@ using System.Net.Http;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves annotation positions from the API.
|
/// Retrieves annotation positions from the API.
|
||||||
/// The URL is composed as <c>{BaseUrl}/api/Annotation/{envelopeKey}</c>.
|
/// Uses relative paths (/api/Annotation/{envelopeKey}).
|
||||||
/// During development, <c>BaseUrl</c> is empty so the request resolves to the
|
|
||||||
/// YARP-proxied route on the same origin, which currently serves
|
|
||||||
/// <c>fake-data/annotations.json</c>. To switch to real data, update the
|
|
||||||
/// YARP route in <c>yarp.json</c> — no code change required.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use SignatureService.")]
|
[Obsolete("Use SignatureService.")]
|
||||||
public class AnnotationService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class AnnotationService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
@@ -10,9 +8,8 @@ public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
|||||||
|
|
||||||
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
||||||
|
|
||||||
public class AuthService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class AuthService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private readonly ApiOptions _api = apiOptions.Value;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
public class DocumentService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class DocumentService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private readonly ApiOptions _api = apiOptions.Value;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches the PDF bytes for the given envelope key from the API.
|
/// Fetches the PDF bytes for the given envelope key from the API.
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ using System.Net.Http;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
|
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
|
||||||
/// from <c>GET api/EnvelopeReceiver/{envelopeKey}</c>.
|
/// from <c>GET /api/EnvelopeReceiver/{envelopeKey}</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
public static class FontLoader {
|
public static class FontLoader
|
||||||
public async static Task LoadFonts(HttpClient httpClient, List<string> fontNames) {
|
{
|
||||||
foreach(var fontName in fontNames) {
|
public static async Task LoadFonts(IHttpClientFactory httpClientFactory, List<string> fontNames)
|
||||||
var fontBytes = await httpClient.GetByteArrayAsync($"fonts/{fontName}");
|
{
|
||||||
|
using var httpClient = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
|
||||||
|
foreach (var fontName in fontNames)
|
||||||
|
{
|
||||||
|
var fontBytes = await httpClient.GetByteArrayAsync($"/fonts/{fontName}");
|
||||||
DXFontRepository.Instance.AddFont(fontBytes);
|
DXFontRepository.Instance.AddFont(fontBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
@@ -9,9 +7,8 @@ namespace EnvelopeGenerator.Server.Client.Services;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client service for managing cached signatures via API.
|
/// Client service for managing cached signatures via API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SignatureCacheService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class SignatureCacheService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private readonly ApiOptions _api = apiOptions.Value;
|
|
||||||
|
|
||||||
public async Task SaveSignatureAsync(
|
public async Task SaveSignatureAsync(
|
||||||
string envelopeKey,
|
string envelopeKey,
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ using System.Net.Http;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
public class SignatureService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
public class SignatureService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
||||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
|
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
|
||||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
|
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ try
|
|||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Load YARP configuration from yarp.json
|
||||||
|
builder.Configuration.AddJsonFile("yarp.json", optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
||||||
|
|
||||||
if (!builder.Environment.IsDevelopment())
|
if (!builder.Environment.IsDevelopment())
|
||||||
@@ -53,8 +56,25 @@ try
|
|||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
// Named HttpClient for internal API calls (same domain, uses relative paths)
|
// YARP Reverse Proxy (for forwarding auth requests to AuthHub)
|
||||||
builder.Services.AddHttpClient("EnvelopeGenerator.Server");
|
builder.Services.AddReverseProxy()
|
||||||
|
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||||
|
|
||||||
|
// HttpContextAccessor needed for SSR HttpClient configuration
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
|
// Named HttpClient for internal API calls
|
||||||
|
builder.Services.AddHttpClient("EnvelopeGenerator.Server", (sp, client) =>
|
||||||
|
{
|
||||||
|
var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
|
||||||
|
var request = httpContextAccessor.HttpContext?.Request;
|
||||||
|
|
||||||
|
if (request != null)
|
||||||
|
{
|
||||||
|
// Set base address to current host for SSR scenarios
|
||||||
|
client.BaseAddress = new Uri($"{request.Scheme}://{request.Host}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// CORS Policy
|
// CORS Policy
|
||||||
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
||||||
@@ -290,9 +310,6 @@ try
|
|||||||
.AddEnvelopeGeneratorServices(config);
|
.AddEnvelopeGeneratorServices(config);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
// HttpClient for server-side components (e.g., MainLayout with FontLoader)
|
|
||||||
builder.Services.AddHttpContextAccessor();
|
|
||||||
|
|
||||||
// Business Services (Server specific)
|
// Business Services (Server specific)
|
||||||
builder.Services.AddScoped<DocumentService>();
|
builder.Services.AddScoped<DocumentService>();
|
||||||
builder.Services.AddScoped<AuthService>();
|
builder.Services.AddScoped<AuthService>();
|
||||||
@@ -302,6 +319,9 @@ try
|
|||||||
builder.Services.AddScoped<SignatureCacheService>();
|
builder.Services.AddScoped<SignatureCacheService>();
|
||||||
builder.Services.AddSingleton<AppVersionService>();
|
builder.Services.AddSingleton<AppVersionService>();
|
||||||
|
|
||||||
|
// SSR Authentication Service (for Envelope Receiver pages)
|
||||||
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.IEnvelopeAuthService, EnvelopeGenerator.Server.Services.EnvelopeAuthService>();
|
||||||
|
|
||||||
// DevExpress Server-Side Services (CRITICAL for DxPdfViewer)
|
// DevExpress Server-Side Services (CRITICAL for DxPdfViewer)
|
||||||
builder.Services.AddDevExpressBlazor();
|
builder.Services.AddDevExpressBlazor();
|
||||||
builder.Services.AddDevExpressServerSideBlazorPdfViewer();
|
builder.Services.AddDevExpressServerSideBlazorPdfViewer();
|
||||||
@@ -360,6 +380,9 @@ try
|
|||||||
// API Controllers (map before Blazor routing)
|
// API Controllers (map before Blazor routing)
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
// YARP Reverse Proxy - forwards unmatched requests to configured backends
|
||||||
|
app.MapReverseProxy();
|
||||||
|
|
||||||
// Blazor routing
|
// Blazor routing
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode()
|
.AddInteractiveServerRenderMode()
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-side authentication service for envelope receiver access validation.
|
||||||
|
/// Uses HttpContext to check JWT claims and envelope key authorization.
|
||||||
|
/// </summary>
|
||||||
|
public class EnvelopeAuthService : IEnvelopeAuthService
|
||||||
|
{
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
private readonly ILogger<EnvelopeAuthService> _logger;
|
||||||
|
|
||||||
|
public EnvelopeAuthService(
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
ILogger<EnvelopeAuthService> logger)
|
||||||
|
{
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsAuthenticated(string envelopeKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(envelopeKey))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("IsAuthenticated called with null or empty envelope key");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = _httpContextAccessor.HttpContext;
|
||||||
|
|
||||||
|
// Check if user is authenticated
|
||||||
|
if (context?.User?.Identity?.IsAuthenticated != true)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("User is not authenticated for envelope {EnvelopeKey}", envelopeKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get envelope key from claims
|
||||||
|
var sub = GetEnvelopeKeyFromClaims(context.User);
|
||||||
|
|
||||||
|
// Verify envelope key matches
|
||||||
|
var isValid = sub == envelopeKey;
|
||||||
|
|
||||||
|
if (!isValid)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Envelope key mismatch: Expected {ExpectedKey}, Got {ActualKey}",
|
||||||
|
envelopeKey,
|
||||||
|
sub ?? "(null)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("User authenticated for envelope {EnvelopeKey}", envelopeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string? GetAuthenticatedEnvelopeKey()
|
||||||
|
{
|
||||||
|
var context = _httpContextAccessor.HttpContext;
|
||||||
|
|
||||||
|
if (context?.User?.Identity?.IsAuthenticated != true)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return GetEnvelopeKeyFromClaims(context.User);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ClaimsPrincipal? GetCurrentUser()
|
||||||
|
{
|
||||||
|
return _httpContextAccessor.HttpContext?.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
// Try NameIdentifier first (standard claim)
|
||||||
|
var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
// Fallback to "sub" claim (JWT standard)
|
||||||
|
if (string.IsNullOrWhiteSpace(sub))
|
||||||
|
{
|
||||||
|
sub = user.FindFirst("sub")?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for handling envelope-specific authentication in SSR (Server-Side Rendering) context.
|
||||||
|
/// </summary>
|
||||||
|
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>
|
||||||
|
/// <param name="envelopeKey">The envelope key to validate against user claims.</param>
|
||||||
|
/// <returns>True if user is authenticated and envelope key matches; otherwise false.</returns>
|
||||||
|
bool IsAuthenticated(string envelopeKey);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated envelope key from the current user's claims (NameIdentifier or "sub" claim).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The envelope key if user is authenticated; otherwise null.</returns>
|
||||||
|
string? GetAuthenticatedEnvelopeKey();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current HttpContext user principal.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>ClaimsPrincipal if available; otherwise null.</returns>
|
||||||
|
ClaimsPrincipal? GetCurrentUser();
|
||||||
|
}
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
{
|
{
|
||||||
"ReverseProxy": {
|
"ReverseProxy": {
|
||||||
"Routes": {
|
"Routes": {
|
||||||
"api-route": {
|
"auth-login": {
|
||||||
"ClusterId": "api-cluster",
|
"ClusterId": "auth-hub",
|
||||||
"Match": {
|
"Match": {
|
||||||
"Path": "/api/{**catch-all}"
|
"Path": "/api/auth",
|
||||||
}
|
"Methods": [ "POST" ]
|
||||||
},
|
},
|
||||||
"swagger-route": {
|
"Transforms": [
|
||||||
"ClusterId": "api-cluster",
|
{ "PathSet": "/api/auth/sign-flow" }
|
||||||
"Match": {
|
]
|
||||||
"Path": "/swagger/{**catch-all}"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"openapi-route": {
|
"auth-envelope-receiver-login": {
|
||||||
"ClusterId": "api-cluster",
|
"ClusterId": "auth-hub",
|
||||||
"Match": {
|
"Match": {
|
||||||
"Path": "/openapi/{**catch-all}"
|
"Path": "/api/Auth/envelope-receiver/{key}",
|
||||||
}
|
"Methods": [ "POST" ]
|
||||||
},
|
},
|
||||||
"scalar-route": {
|
"Transforms": [
|
||||||
"ClusterId": "api-cluster",
|
{ "PathPattern": "/api/auth/envelope-receiver/{key}" },
|
||||||
"Match": {
|
{
|
||||||
"Path": "/scalar/{**catch-all}"
|
"QueryValueParameter": "cookie",
|
||||||
|
"Set": "true"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Clusters": {
|
"Clusters": {
|
||||||
"api-cluster": {
|
"auth-hub": {
|
||||||
"Destinations": {
|
"Destinations": {
|
||||||
"api-destination": {
|
"primary": {
|
||||||
"Address": "https://localhost:8088"
|
"Address": "https://localhost:9090"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B29
|
|||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
||||||
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
||||||
|
OPEN_SSR_TASK.md = OPEN_SSR_TASK.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
||||||
|
|||||||
553
OPEN_SSR_TASK.md
Normal file
553
OPEN_SSR_TASK.md
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
# 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
|
||||||
Reference in New Issue
Block a user