Compare commits
5 Commits
master
...
feat/blazo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f3c66b4f7 | ||
|
|
7271a92d32 | ||
|
|
c7275ad966 | ||
|
|
bf8115259a | ||
|
|
590ab9bf02 |
@@ -0,0 +1,46 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Services;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Fragt die API, ob der Nutzer eingeloggt ist.
|
||||
///
|
||||
/// WARUM nicht selbst Token lesen?
|
||||
/// - Das Auth-Cookie ist HttpOnly → JavaScript/WASM kann es nicht lesen
|
||||
/// - Stattdessen: Frage die API "bin ich eingeloggt?" → GET /api/auth/check
|
||||
/// - Die API prüft das Cookie serverseitig und antwortet mit 200 oder 401
|
||||
/// </summary>
|
||||
public class ApiAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
public ApiAuthStateProvider(IAuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
var result = await _authService.CheckAuthAsync();
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
// Eingeloggt → Erstelle einen authentifizierten ClaimsPrincipal
|
||||
var identity = new ClaimsIdentity("cookie");
|
||||
return new AuthenticationState(new ClaimsPrincipal(identity));
|
||||
}
|
||||
|
||||
// Nicht eingeloggt
|
||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wird nach Login/Logout aufgerufen, damit Blazor den Auth-State aktualisiert.
|
||||
/// </summary>
|
||||
public void NotifyAuthChanged()
|
||||
{
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
@* DUMB COMPONENT: Kennt keine Services, nur Parameter und Events *@
|
||||
|
||||
<div class="access-code-container">
|
||||
<h2>Zugangscode eingeben</h2>
|
||||
<p>Ein Zugangscode wurde an Ihre E-Mail-Adresse gesendet.</p>
|
||||
|
||||
<EditForm Model="_model" OnValidSubmit="Submit">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="form-group">
|
||||
<InputText @bind-Value="_model.Code"
|
||||
class="form-control code-input"
|
||||
placeholder="000000"
|
||||
maxlength="6" />
|
||||
<ValidationMessage For="() => _model.Code" />
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger mt-2">@ErrorMessage</div>
|
||||
}
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-3" disabled="@_isSubmitting">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<LoadingIndicator Small="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Bestätigen</span>
|
||||
}
|
||||
</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// Parameter von der Eltern-Page
|
||||
[Parameter] public required string EnvelopeKey { get; set; }
|
||||
[Parameter] public string? ErrorMessage { get; set; }
|
||||
|
||||
// EventCallback: Informiert die Page, dass ein Code eingegeben wurde
|
||||
[Parameter] public EventCallback<string> OnSubmit { get; set; }
|
||||
|
||||
private AccessCodeModel _model = new();
|
||||
private bool _isSubmitting;
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
_isSubmitting = true;
|
||||
await OnSubmit.InvokeAsync(_model.Code);
|
||||
_isSubmitting = false;
|
||||
}
|
||||
|
||||
private class AccessCodeModel
|
||||
{
|
||||
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Bitte Zugangscode eingeben")]
|
||||
[System.ComponentModel.DataAnnotations.StringLength(6, MinimumLength = 4)]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@inject IJSRuntime JS
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<div id="pspdfkit-container" class="pdf-container" style="width: 100%; height: 80vh;"></div>
|
||||
|
||||
@code {
|
||||
[Parameter] public byte[]? DocumentBytes { get; set; }
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && DocumentBytes is not null)
|
||||
{
|
||||
// TODO: PSPDFKit JS-Interop implementieren (Phase 6)
|
||||
// await JS.InvokeVoidAsync("initPdfViewer", DocumentBytes);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// TODO: PSPDFKit aufräumen
|
||||
// await JS.InvokeVoidAsync("destroyPdfViewer");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>SignaturePanel</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>TwoFactorForm</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>NavHeader</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>AlertMessage</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="text-center py-5">
|
||||
@if (!string.IsNullOrEmpty(Icon))
|
||||
{
|
||||
<div class="mb-3">
|
||||
<i class="bi bi-@Icon" style="font-size: 3rem;"></i>
|
||||
</div>
|
||||
}
|
||||
<h2>@Title</h2>
|
||||
@if (!string.IsNullOrEmpty(Message))
|
||||
{
|
||||
<p class="text-muted">@Message</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Title { get; set; } = "Fehler";
|
||||
[Parameter] public string? Message { get; set; }
|
||||
[Parameter] public string? Icon { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>LanguageSelector</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="d-flex justify-content-center align-items-center @(Small ? "" : "py-5")" style="@(Small ? "" : "min-height: 40vh;")">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border @(Small ? "spinner-border-sm" : "text-primary")"
|
||||
style="@(Small ? "" : "width: 3rem; height: 3rem;")"
|
||||
role="status">
|
||||
<span class="visually-hidden">Laden...</span>
|
||||
</div>
|
||||
@if (!Small && Message is not null)
|
||||
{
|
||||
<p class="mt-3 text-muted">@Message</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public bool Small { get; set; }
|
||||
[Parameter] public string? Message { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Hält den aktuellen Authentifizierungs-Zustand im Client.
|
||||
/// Wird vom ApiAuthStateProvider gesetzt und von Komponenten gelesen.
|
||||
/// </summary>
|
||||
public class AuthState
|
||||
{
|
||||
public bool IsAuthenticated { get; set; }
|
||||
public string? Role { get; set; }
|
||||
public string? EnvelopeUuid { get; set; }
|
||||
public string? ReceiverEmail { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-seitiges DTO für Dokument-Daten.
|
||||
/// </summary>
|
||||
public record DocumentModel
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public int EnvelopeId { get; init; }
|
||||
public DateTime AddedWhen { get; init; }
|
||||
public byte[]? ByteData { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-seitiges DTO für Umschlag-Daten.
|
||||
/// Muss nur die JSON-Properties matchen, die die API zurückgibt
|
||||
/// und die der Client tatsächlich braucht.
|
||||
///
|
||||
/// WARUM eigene DTOs statt die aus EnvelopeGenerator.Application?
|
||||
/// - Application hat Server-Abhängigkeiten (SqlClient, JwtBearer, EF Core)
|
||||
/// - Diese Pakete existieren nicht für browser-wasm → Build-Fehler
|
||||
/// - Der Client braucht nur eine Teilmenge der Felder
|
||||
/// - Eigene DTOs machen den Client unabhängig vom Server
|
||||
/// </summary>
|
||||
public record EnvelopeModel
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Uuid { get; init; } = string.Empty;
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public bool UseAccessCode { get; init; }
|
||||
public bool TFAEnabled { get; init; }
|
||||
public bool ReadOnly { get; init; }
|
||||
public string Language { get; init; } = "de-DE";
|
||||
public DateTime AddedWhen { get; init; }
|
||||
public UserModel? User { get; init; }
|
||||
public IEnumerable<DocumentModel>? Documents { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-seitiges DTO für die Envelope-Receiver-Zuordnung.
|
||||
/// </summary>
|
||||
public record EnvelopeReceiverModel
|
||||
{
|
||||
public EnvelopeModel? Envelope { get; init; }
|
||||
public ReceiverModel? Receiver { get; init; }
|
||||
public int EnvelopeId { get; init; }
|
||||
public int ReceiverId { get; init; }
|
||||
public int Sequence { get; init; }
|
||||
public string? Name { get; init; }
|
||||
public bool HasPhoneNumber { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Models
|
||||
{
|
||||
public class EnvelopeViewModel
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-seitiges DTO für Empfänger-Daten.
|
||||
/// </summary>
|
||||
public record ReceiverModel
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string EmailAddress { get; init; } = string.Empty;
|
||||
public string Signature { get; init; } = string.Empty;
|
||||
public DateTime AddedWhen { get; init; }
|
||||
public DateTime? TfaRegDeadline { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-seitiges DTO für Benutzer-Daten (Absender).
|
||||
/// </summary>
|
||||
public record UserModel
|
||||
{
|
||||
public string? Email { get; init; }
|
||||
public string? DisplayName { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>EnvelopeExpired</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>EnvelopeLocked</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
@page "/envelope/{EnvelopeKey}"
|
||||
@rendermode InteractiveAuto
|
||||
@inject IEnvelopeService EnvelopeService
|
||||
@inject EnvelopeState State
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>Dokument</PageTitle>
|
||||
|
||||
@switch (State.Status)
|
||||
{
|
||||
case EnvelopePageStatus.Loading:
|
||||
<LoadingIndicator Message="Dokument wird geladen..." />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.NotFound:
|
||||
<ErrorDisplay Title="Nicht gefunden"
|
||||
Message="Dieses Dokument existiert nicht oder ist nicht mehr verfügbar." />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.AlreadySigned:
|
||||
<ErrorDisplay Title="Bereits unterschrieben"
|
||||
Message="Dieses Dokument wurde bereits unterschrieben."
|
||||
Icon="check-circle" />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.RequiresAccessCode:
|
||||
<AccessCodeForm EnvelopeKey="@EnvelopeKey"
|
||||
ErrorMessage="@State.ErrorMessage"
|
||||
OnSubmit="HandleAccessCodeSubmit" />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.ShowDocument:
|
||||
<PdfViewer DocumentBytes="@_documentBytes" />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.Error:
|
||||
<ErrorDisplay Title="Fehler" Message="@State.ErrorMessage" />
|
||||
break;
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string EnvelopeKey { get; set; } = default!;
|
||||
|
||||
private byte[]? _documentBytes;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
State.OnChange += StateHasChanged;
|
||||
await LoadEnvelopeAsync();
|
||||
}
|
||||
|
||||
private async Task LoadEnvelopeAsync()
|
||||
{
|
||||
State.SetLoading();
|
||||
|
||||
// Die genaue API-Logik hängt von den verfügbaren Endpunkten ab.
|
||||
// Dies ist die Struktur — die konkreten Endpoints implementierst du
|
||||
// basierend auf den vorhandenen API-Controllern.
|
||||
var result = await EnvelopeService.GetEnvelopeReceiversAsync();
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
if (result.StatusCode == 401)
|
||||
State.SetAccessCodeRequired();
|
||||
else if (result.StatusCode == 404)
|
||||
State.SetNotFound();
|
||||
else
|
||||
State.SetError(result.ErrorMessage ?? "Unbekannter Fehler");
|
||||
return;
|
||||
}
|
||||
|
||||
// Daten verarbeiten und Status setzen
|
||||
State.SetDocument();
|
||||
}
|
||||
|
||||
private async Task HandleAccessCodeSubmit(string code)
|
||||
{
|
||||
// AccessCode an API senden
|
||||
// Bei Erfolg: State.SetDocument() oder State.SetTwoFactorRequired()
|
||||
// Bei Fehler: State.SetError(...)
|
||||
}
|
||||
|
||||
public void Dispose() => State.OnChange -= StateHasChanged;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>EnvelopeRejected</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>EnvelopeSigned</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>Home</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>NotFound</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Auth;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Services;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.State;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
// HttpClient: BaseAddress zeigt auf den ReceiverUI-Server (gleiche Domain)
|
||||
// Von dort werden alle /api/* Calls via YARP an die echte API weitergeleitet
|
||||
builder.Services.AddScoped(sp =>
|
||||
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
|
||||
// Auth: Blazor fragt über diesen Provider "Ist der Nutzer eingeloggt?"
|
||||
builder.Services.AddAuthorizationCore();
|
||||
builder.Services.AddScoped<ApiAuthStateProvider>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp =>
|
||||
sp.GetRequiredService<ApiAuthStateProvider>());
|
||||
|
||||
// API-Services: Je ein Service pro API-Controller
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddScoped<IEnvelopeService, EnvelopeService>();
|
||||
|
||||
// State: Ein State-Objekt pro Browser-Tab
|
||||
builder.Services.AddScoped<EnvelopeState>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,54 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Spricht mit dem bestehenden AuthController der API.
|
||||
/// Die API erkennt den Nutzer über das Cookie "AuthToken" automatisch.
|
||||
/// </summary>
|
||||
public class AuthService : ApiServiceBase, IAuthService
|
||||
{
|
||||
public AuthService(HttpClient http, ILogger<AuthService> logger) : base(http, logger) { }
|
||||
|
||||
public async Task<ApiResponse> CheckAuthAsync(string? role = null, CancellationToken ct = default)
|
||||
{
|
||||
var endpoint = role is not null ? $"api/auth/check?role={role}" : "api/auth/check";
|
||||
try
|
||||
{
|
||||
var response = await Http.GetAsync(endpoint, ct);
|
||||
return response.IsSuccessStatusCode
|
||||
? ApiResponse.Success((int)response.StatusCode)
|
||||
: ApiResponse.Failure((int)response.StatusCode);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Logger.LogError(ex, "HTTP error calling GET {Endpoint}", endpoint);
|
||||
return ApiResponse.Failure(0, "Verbindung zum Server fehlgeschlagen.");
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return ApiResponse.Failure(0, "Anfrage abgebrochen.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> LogoutAsync(CancellationToken ct = default)
|
||||
{
|
||||
const string endpoint = "api/auth/logout";
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsync(endpoint, null, ct);
|
||||
return response.IsSuccessStatusCode
|
||||
? ApiResponse.Success((int)response.StatusCode)
|
||||
: ApiResponse.Failure((int)response.StatusCode);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Logger.LogError(ex, "HTTP error calling POST {Endpoint}", endpoint);
|
||||
return ApiResponse.Failure(0, "Verbindung zum Server fehlgeschlagen.");
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return ApiResponse.Failure(0, "Anfrage abgebrochen.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services.Base;
|
||||
|
||||
/// <summary>
|
||||
/// Einheitliches Response-Objekt für ALLE API-Aufrufe.
|
||||
///
|
||||
/// WARUM: Jeder API-Aufruf kann fehlschlagen (Netzwerk, 401, 500...).
|
||||
/// Statt überall try-catch zu haben, kapselt dieses Objekt Erfolg/Fehler einheitlich.
|
||||
/// So kann jede Blazor-Komponente einheitlich darauf reagieren.
|
||||
/// </summary>
|
||||
public record ApiResponse<T>
|
||||
{
|
||||
public bool IsSuccess { get; init; }
|
||||
public T? Data { get; init; }
|
||||
public int StatusCode { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
|
||||
public static ApiResponse<T> Success(T data, int statusCode = 200)
|
||||
=> new() { IsSuccess = true, Data = data, StatusCode = statusCode };
|
||||
|
||||
public static ApiResponse<T> Failure(int statusCode, string? error = null)
|
||||
=> new() { IsSuccess = false, StatusCode = statusCode, ErrorMessage = error };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response ohne Daten (für POST/PUT/DELETE die nur Status zurückgeben).
|
||||
/// </summary>
|
||||
public record ApiResponse
|
||||
{
|
||||
public bool IsSuccess { get; init; }
|
||||
public int StatusCode { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
|
||||
public static ApiResponse Success(int statusCode = 200)
|
||||
=> new() { IsSuccess = true, StatusCode = statusCode };
|
||||
|
||||
public static ApiResponse Failure(int statusCode, string? error = null)
|
||||
=> new() { IsSuccess = false, StatusCode = statusCode, ErrorMessage = error };
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services.Base;
|
||||
|
||||
/// <summary>
|
||||
/// Basisklasse für ALLE API-Services.
|
||||
///
|
||||
/// WARUM eine Basisklasse?
|
||||
/// - Einheitliches Error-Handling: Jeder API-Aufruf wird gleich behandelt
|
||||
/// - DRY (Don't Repeat Yourself): Logging, Fehlerbehandlung, Serialisierung nur einmal
|
||||
/// - Einfache Erweiterung: Retry-Logik, Token-Refresh etc. nur hier ändern
|
||||
/// </summary>
|
||||
public abstract class ApiServiceBase
|
||||
{
|
||||
protected readonly HttpClient Http;
|
||||
protected readonly ILogger Logger;
|
||||
|
||||
protected ApiServiceBase(HttpClient http, ILogger logger)
|
||||
{
|
||||
Http = http;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET-Request mit Deserialisierung.
|
||||
/// Alle API GET-Aufrufe gehen durch diese Methode.
|
||||
/// </summary>
|
||||
protected async Task<ApiResponse<T>> GetAsync<T>(string endpoint, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await Http.GetAsync(endpoint, ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorBody = await response.Content.ReadAsStringAsync(ct);
|
||||
Logger.LogWarning("GET {Endpoint} failed: {Status} - {Body}",
|
||||
endpoint, (int)response.StatusCode, errorBody);
|
||||
return ApiResponse<T>.Failure((int)response.StatusCode, errorBody);
|
||||
}
|
||||
|
||||
var data = await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct);
|
||||
return ApiResponse<T>.Success(data!, (int)response.StatusCode);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Logger.LogError(ex, "HTTP error calling GET {Endpoint}", endpoint);
|
||||
return ApiResponse<T>.Failure(0, "Verbindung zum Server fehlgeschlagen.");
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
Logger.LogWarning("GET {Endpoint} was cancelled", endpoint);
|
||||
return ApiResponse<T>.Failure(0, "Anfrage abgebrochen.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST-Request mit Body und Response-Deserialisierung.
|
||||
/// </summary>
|
||||
protected async Task<ApiResponse<TResponse>> PostAsync<TRequest, TResponse>(
|
||||
string endpoint, TRequest body, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsJsonAsync(endpoint, body, ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorBody = await response.Content.ReadAsStringAsync(ct);
|
||||
Logger.LogWarning("POST {Endpoint} failed: {Status} - {Body}",
|
||||
endpoint, (int)response.StatusCode, errorBody);
|
||||
return ApiResponse<TResponse>.Failure((int)response.StatusCode, errorBody);
|
||||
}
|
||||
|
||||
var data = await response.Content.ReadFromJsonAsync<TResponse>(cancellationToken: ct);
|
||||
return ApiResponse<TResponse>.Success(data!, (int)response.StatusCode);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Logger.LogError(ex, "HTTP error calling POST {Endpoint}", endpoint);
|
||||
return ApiResponse<TResponse>.Failure(0, "Verbindung zum Server fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST-Request ohne Response-Body (z.B. Logout).
|
||||
/// </summary>
|
||||
protected async Task<ApiResponse> PostAsync<TRequest>(
|
||||
string endpoint, TRequest body, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsJsonAsync(endpoint, body, ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorBody = await response.Content.ReadAsStringAsync(ct);
|
||||
return ApiResponse.Failure((int)response.StatusCode, errorBody);
|
||||
}
|
||||
|
||||
return ApiResponse.Success((int)response.StatusCode);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Logger.LogError(ex, "HTTP error calling POST {Endpoint}", endpoint);
|
||||
return ApiResponse.Failure(0, "Verbindung zum Server fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
|
||||
|
||||
public class EnvelopeService : ApiServiceBase, IEnvelopeService
|
||||
{
|
||||
public EnvelopeService(HttpClient http, ILogger<EnvelopeService> logger) : base(http, logger) { }
|
||||
|
||||
public Task<ApiResponse<IEnumerable<EnvelopeModel>>> GetEnvelopesAsync(CancellationToken ct = default)
|
||||
=> GetAsync<IEnumerable<EnvelopeModel>>("api/envelope", ct);
|
||||
|
||||
public Task<ApiResponse<IEnumerable<EnvelopeReceiverModel>>> GetEnvelopeReceiversAsync(
|
||||
CancellationToken ct = default)
|
||||
=> GetAsync<IEnumerable<EnvelopeReceiverModel>>("api/envelopereceiver", ct);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services
|
||||
{
|
||||
public class HistoryService
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Kommuniziert mit dem AuthController der API.
|
||||
///
|
||||
/// WARUM Interface + Implementierung?
|
||||
/// - Testbarkeit: In Unit-Tests kann man einen Mock verwenden
|
||||
/// - Austauschbarkeit: Wenn sich die API ändert, ändert sich nur die Implementierung
|
||||
/// - Blazor-Konvention: Services werden über Interfaces per DI registriert
|
||||
/// </summary>
|
||||
public interface IAuthService
|
||||
{
|
||||
/// <summary>Prüft ob der Nutzer eingeloggt ist → GET /api/auth/check</summary>
|
||||
Task<ApiResponse> CheckAuthAsync(string? role = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Logout → POST /api/auth/logout</summary>
|
||||
Task<ApiResponse> LogoutAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Kommuniziert mit EnvelopeController und EnvelopeReceiverController.
|
||||
/// Verwendet Client-eigene Models statt der Server-DTOs.
|
||||
/// </summary>
|
||||
public interface IEnvelopeService
|
||||
{
|
||||
/// <summary>Lädt Umschläge → GET /api/envelope</summary>
|
||||
Task<ApiResponse<IEnumerable<EnvelopeModel>>> GetEnvelopesAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Lädt EnvelopeReceiver → GET /api/envelopereceiver</summary>
|
||||
Task<ApiResponse<IEnumerable<EnvelopeReceiverModel>>> GetEnvelopeReceiversAsync(
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services
|
||||
{
|
||||
public interface IHistoryService
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.State
|
||||
{
|
||||
public class AuthState
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.State;
|
||||
|
||||
/// <summary>
|
||||
/// Hält den aktuellen Zustand des geladenen Umschlags.
|
||||
///
|
||||
/// WARUM ein eigenes State-Objekt?
|
||||
/// - Mehrere Komponenten auf einer Seite brauchen die gleichen Daten
|
||||
/// - Ohne State müsste jede Komponente die Daten selbst laden → doppelte API-Calls
|
||||
/// - StateHasChanged() informiert automatisch alle Subscriber
|
||||
///
|
||||
/// PATTERN: "Observable State" — Services setzen den State, Komponenten reagieren darauf.
|
||||
/// </summary>
|
||||
public class EnvelopeState
|
||||
{
|
||||
private EnvelopePageStatus _status = EnvelopePageStatus.Loading;
|
||||
private string? _errorMessage;
|
||||
|
||||
/// <summary>Aktueller Seitenstatus</summary>
|
||||
public EnvelopePageStatus Status
|
||||
{
|
||||
get => _status;
|
||||
private set
|
||||
{
|
||||
_status = value;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fehlermeldung (falls vorhanden)</summary>
|
||||
public string? ErrorMessage
|
||||
{
|
||||
get => _errorMessage;
|
||||
private set
|
||||
{
|
||||
_errorMessage = value;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Zustandsübergänge (öffentliche Methoden) ---
|
||||
|
||||
public void SetLoading() => Status = EnvelopePageStatus.Loading;
|
||||
|
||||
public void SetAccessCodeRequired()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
Status = EnvelopePageStatus.RequiresAccessCode;
|
||||
}
|
||||
|
||||
public void SetTwoFactorRequired() => Status = EnvelopePageStatus.RequiresTwoFactor;
|
||||
|
||||
public void SetDocument() => Status = EnvelopePageStatus.ShowDocument;
|
||||
|
||||
public void SetError(string message)
|
||||
{
|
||||
ErrorMessage = message;
|
||||
Status = EnvelopePageStatus.Error;
|
||||
}
|
||||
|
||||
public void SetAlreadySigned() => Status = EnvelopePageStatus.AlreadySigned;
|
||||
public void SetRejected() => Status = EnvelopePageStatus.Rejected;
|
||||
public void SetNotFound() => Status = EnvelopePageStatus.NotFound;
|
||||
|
||||
// --- Event: Benachrichtigt Komponenten über Änderungen ---
|
||||
public event Action? OnChange;
|
||||
private void NotifyStateChanged() => OnChange?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Alle möglichen Zustände der Umschlag-Seite</summary>
|
||||
public enum EnvelopePageStatus
|
||||
{
|
||||
Loading,
|
||||
RequiresAccessCode,
|
||||
RequiresTwoFactor,
|
||||
ShowDocument,
|
||||
AlreadySigned,
|
||||
Rejected,
|
||||
NotFound,
|
||||
Expired,
|
||||
Error
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using EnvelopeGenerator.ReceiverUI.Client
|
||||
@using EnvelopeGenerator.ReceiverUI.Client.Models
|
||||
@using EnvelopeGenerator.ReceiverUI.Client.Services
|
||||
@using EnvelopeGenerator.ReceiverUI.Client.Services.Base
|
||||
@using EnvelopeGenerator.ReceiverUI.Client.State
|
||||
@using EnvelopeGenerator.ReceiverUI.Client.Auth
|
||||
@using EnvelopeGenerator.ReceiverUI.Client.Components.Shared
|
||||
@using EnvelopeGenerator.ReceiverUI.Client.Components.Envelope
|
||||
@@ -0,0 +1,2 @@
|
||||
body {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="EnvelopeGenerator.ReceiverUI.styles.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<h3>AuthLayout</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="app-container">
|
||||
<header class="app-header">
|
||||
<div class="header-content">
|
||||
<span class="app-title">signFLOW</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<ErrorBoundary @ref="_errorBoundary">
|
||||
<ChildContent>
|
||||
@Body
|
||||
</ChildContent>
|
||||
<ErrorContent Context="ex">
|
||||
<div class="error-container text-center py-5">
|
||||
<h2>😵 Ein unerwarteter Fehler ist aufgetreten</h2>
|
||||
<p class="text-muted">Bitte versuchen Sie es erneut.</p>
|
||||
<button class="btn btn-primary" @onclick="Recover">Erneut versuchen</button>
|
||||
</div>
|
||||
</ErrorContent>
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer text-center py-2 text-muted">
|
||||
<small>© @DateTime.Now.Year Digital Data GmbH</small>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private ErrorBoundary? _errorBoundary;
|
||||
|
||||
private void Recover() => _errorBoundary?.Recover();
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
@@ -0,0 +1,14 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="typeof(Layout.MainLayout)">
|
||||
<div class="text-center py-5">
|
||||
<h1>404</h1>
|
||||
<p>Diese Seite wurde nicht gefunden.</p>
|
||||
</div>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@@ -0,0 +1,9 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using EnvelopeGenerator.ReceiverUI.Components
|
||||
@using EnvelopeGenerator.ReceiverUI.Components.Layout
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.ReceiverUI.Client\EnvelopeGenerator.ReceiverUI.Client.csproj" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.3" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,42 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Components;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents()
|
||||
.AddInteractiveWebAssemblyComponents();
|
||||
|
||||
// API-Proxy: Alle /api/* Aufrufe an die echte API weiterleiten
|
||||
// WARUM: Der Blazor-Client ruft /api/envelope auf. Diese Anfrage geht an den
|
||||
// ReceiverUI-Server (gleiche Domain, kein CORS), der sie an die echte API weiterleitet.
|
||||
var apiBaseUrl = builder.Configuration["ApiBaseUrl"]
|
||||
?? throw new InvalidOperationException("ApiBaseUrl is not configured in appsettings.json.");
|
||||
|
||||
builder.Services.AddHttpForwarder();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseWebAssemblyDebugging();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
|
||||
// Alle /api/* Requests an die echte EnvelopeGenerator.API weiterleiten
|
||||
// So muss der Browser nie direkt mit der API sprechen → kein CORS, Cookies funktionieren
|
||||
app.MapForwarder("/api/{**catch-all}", apiBaseUrl);
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
.AddAdditionalAssemblies(typeof(EnvelopeGenerator.ReceiverUI.Client._Imports).Assembly);
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:3101",
|
||||
"sslPort": 44303
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5109",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:7206;http://localhost:5109",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ApiBaseUrl": "https://localhost:5001",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
|
||||
@@ -39,6 +39,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.WorkerSer
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.API", "EnvelopeGenerator.API\EnvelopeGenerator.API.csproj", "{EC768913-6270-14F4-1DD3-69C87A659462}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{620A0476-2F37-490D-ABE9-50DEEB217DA7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI.Client", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.Client\EnvelopeGenerator.ReceiverUI.Client.csproj", "{62DD9063-C026-4805-BD6D-4DB899ABC504}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -97,6 +101,14 @@ Global
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{620A0476-2F37-490D-ABE9-50DEEB217DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{620A0476-2F37-490D-ABE9-50DEEB217DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{620A0476-2F37-490D-ABE9-50DEEB217DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{620A0476-2F37-490D-ABE9-50DEEB217DA7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{62DD9063-C026-4805-BD6D-4DB899ABC504}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{62DD9063-C026-4805-BD6D-4DB899ABC504}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{62DD9063-C026-4805-BD6D-4DB899ABC504}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{62DD9063-C026-4805-BD6D-4DB899ABC504}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -118,6 +130,8 @@ Global
|
||||
{3D0514EA-2681-4B13-AD71-35CC6363DBD7} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
|
||||
{E3676510-7030-4E85-86E1-51E483E2A3B6} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{620A0476-2F37-490D-ABE9-50DEEB217DA7} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{62DD9063-C026-4805-BD6D-4DB899ABC504} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}
|
||||
|
||||
Reference in New Issue
Block a user