diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Auth/ApiAuthStateProvider.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Auth/ApiAuthStateProvider.cs new file mode 100644 index 00000000..9604e409 --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Auth/ApiAuthStateProvider.cs @@ -0,0 +1,46 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using EnvelopeGenerator.ReceiverUI.Client.Services; + +namespace EnvelopeGenerator.ReceiverUI.Client.Auth; + +/// +/// 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 +/// +public class ApiAuthStateProvider : AuthenticationStateProvider +{ + private readonly IAuthService _authService; + + public ApiAuthStateProvider(IAuthService authService) + { + _authService = authService; + } + + public override async Task 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())); + } + + /// + /// Wird nach Login/Logout aufgerufen, damit Blazor den Auth-State aktualisiert. + /// + public void NotifyAuthChanged() + { + NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/AccessCodeForm.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/AccessCodeForm.razor index 70d23e4b..1113b905 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/AccessCodeForm.razor +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/AccessCodeForm.razor @@ -1,5 +1,60 @@ -

AccessCodeForm

+@* DUMB COMPONENT: Kennt keine Services, nur Parameter und Events *@ + +
+

Zugangscode eingeben

+

Ein Zugangscode wurde an Ihre E-Mail-Adresse gesendet.

+ + + + +
+ + +
+ + @if (!string.IsNullOrEmpty(ErrorMessage)) + { +
@ErrorMessage
+ } + + +
+
@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 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; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/PdfViewer.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/PdfViewer.razor index 0afc1e16..5d115ac2 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/PdfViewer.razor +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/PdfViewer.razor @@ -1,5 +1,23 @@ -

PdfViewer

+@inject IJSRuntime JS +@implements IAsyncDisposable + +
@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"); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/ErrorDisplay.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/ErrorDisplay.razor index 7eb6305c..7c07858a 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/ErrorDisplay.razor +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/ErrorDisplay.razor @@ -1,5 +1,19 @@ -

ErrorDisplay

+
+ @if (!string.IsNullOrEmpty(Icon)) + { +
+ +
+ } +

@Title

+ @if (!string.IsNullOrEmpty(Message)) + { +

@Message

+ } +
@code { - -} + [Parameter] public string Title { get; set; } = "Fehler"; + [Parameter] public string? Message { get; set; } + [Parameter] public string? Icon { get; set; } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/LoadingIndicator.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/LoadingIndicator.razor index 1e8bfcdc..66ee63ce 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/LoadingIndicator.razor +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/LoadingIndicator.razor @@ -1,5 +1,18 @@ -

LoadingIndicator

+
+
+
+ Laden... +
+ @if (!Small && Message is not null) + { +

@Message

+ } +
+
@code { - -} + [Parameter] public bool Small { get; set; } + [Parameter] public string? Message { get; set; } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/EnvelopeGenerator.ReceiverUI.Client.csproj b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/EnvelopeGenerator.ReceiverUI.Client.csproj index bb285f92..c9d2a4c6 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/EnvelopeGenerator.ReceiverUI.Client.csproj +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/EnvelopeGenerator.ReceiverUI.Client.csproj @@ -1,15 +1,17 @@ - - net8.0 - enable - enable - true - Default - + + net9.0 + enable + enable + true + Default + - - - + + + + + - + \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/AccessCodeModel.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/AccessCodeModel.cs deleted file mode 100644 index 889a95da..00000000 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/AccessCodeModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace EnvelopeGenerator.ReceiverUI.Client.Models -{ - public class AccessCodeModel - { - } -} diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/AuthState.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/AuthState.cs index 3310f97e..ea19d7fd 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/AuthState.cs +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/AuthState.cs @@ -1,6 +1,13 @@ -namespace EnvelopeGenerator.ReceiverUI.Client.Models +namespace EnvelopeGenerator.ReceiverUI.Client.Models; + +/// +/// Hält den aktuellen Authentifizierungs-Zustand im Client. +/// Wird vom ApiAuthStateProvider gesetzt und von Komponenten gelesen. +/// +public class AuthState { - public class AuthState - { - } -} + public bool IsAuthenticated { get; set; } + public string? Role { get; set; } + public string? EnvelopeUuid { get; set; } + public string? ReceiverEmail { get; set; } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/DocumentModel.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/DocumentModel.cs new file mode 100644 index 00000000..13a10ef5 --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/DocumentModel.cs @@ -0,0 +1,12 @@ +namespace EnvelopeGenerator.ReceiverUI.Client.Models; + +/// +/// Client-seitiges DTO für Dokument-Daten. +/// +public record DocumentModel +{ + public int Id { get; init; } + public int EnvelopeId { get; init; } + public DateTime AddedWhen { get; init; } + public byte[]? ByteData { get; init; } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/EnvelopeModel.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/EnvelopeModel.cs new file mode 100644 index 00000000..c3e10a5c --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/EnvelopeModel.cs @@ -0,0 +1,27 @@ +namespace EnvelopeGenerator.ReceiverUI.Client.Models; + +/// +/// 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 +/// +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? Documents { get; init; } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/EnvelopeReceiverModel.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/EnvelopeReceiverModel.cs new file mode 100644 index 00000000..8905e82a --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/EnvelopeReceiverModel.cs @@ -0,0 +1,15 @@ +namespace EnvelopeGenerator.ReceiverUI.Client.Models; + +/// +/// Client-seitiges DTO für die Envelope-Receiver-Zuordnung. +/// +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; } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/ReceiverModel.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/ReceiverModel.cs new file mode 100644 index 00000000..2490ce4f --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/ReceiverModel.cs @@ -0,0 +1,13 @@ +namespace EnvelopeGenerator.ReceiverUI.Client.Models; + +/// +/// Client-seitiges DTO für Empfänger-Daten. +/// +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; } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/UserModel.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/UserModel.cs new file mode 100644 index 00000000..cc06e836 --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Models/UserModel.cs @@ -0,0 +1,10 @@ +namespace EnvelopeGenerator.ReceiverUI.Client.Models; + +/// +/// Client-seitiges DTO für Benutzer-Daten (Absender). +/// +public record UserModel +{ + public string? Email { get; init; } + public string? DisplayName { get; init; } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Pages/Envelope/EnvelopePage.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Pages/Envelope/EnvelopePage.razor index eeee013e..aea38073 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Pages/Envelope/EnvelopePage.razor +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Pages/Envelope/EnvelopePage.razor @@ -1,5 +1,84 @@ -

EnvelopePage

+@page "/envelope/{EnvelopeKey}" +@rendermode InteractiveAuto +@inject IEnvelopeService EnvelopeService +@inject EnvelopeState State +@implements IDisposable + +Dokument + +@switch (State.Status) +{ + case EnvelopePageStatus.Loading: + + break; + + case EnvelopePageStatus.NotFound: + + break; + + case EnvelopePageStatus.AlreadySigned: + + break; + + case EnvelopePageStatus.RequiresAccessCode: + + break; + + case EnvelopePageStatus.ShowDocument: + + break; + + case EnvelopePageStatus.Error: + + 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; +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Program.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Program.cs index 519269f2..fc688bf6 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Program.cs +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Program.cs @@ -1,5 +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); -await builder.Build().RunAsync(); +// 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(); +builder.Services.AddScoped(sp => + sp.GetRequiredService()); + +// API-Services: Je ein Service pro API-Controller +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// State: Ein State-Objekt pro Browser-Tab +builder.Services.AddScoped(); + +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/AuthService.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/AuthService.cs index ede360bc..924fbff3 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/AuthService.cs +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/AuthService.cs @@ -1,6 +1,54 @@ -namespace EnvelopeGenerator.ReceiverUI.Client.Services +using EnvelopeGenerator.ReceiverUI.Client.Services.Base; + +namespace EnvelopeGenerator.ReceiverUI.Client.Services; + +/// +/// Spricht mit dem bestehenden AuthController der API. +/// Die API erkennt den Nutzer über das Cookie "AuthToken" automatisch. +/// +public class AuthService : ApiServiceBase, IAuthService { - public class AuthService + public AuthService(HttpClient http, ILogger logger) : base(http, logger) { } + + public async Task 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 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."); + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/Base/ApiResponse.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/Base/ApiResponse.cs index ecbdd497..747422fa 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/Base/ApiResponse.cs +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/Base/ApiResponse.cs @@ -1,6 +1,38 @@ -namespace EnvelopeGenerator.ReceiverUI.Client.Services.Base +namespace EnvelopeGenerator.ReceiverUI.Client.Services.Base; + +/// +/// 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. +/// +public record ApiResponse { - public class ApiResponse - { - } + public bool IsSuccess { get; init; } + public T? Data { get; init; } + public int StatusCode { get; init; } + public string? ErrorMessage { get; init; } + + public static ApiResponse Success(T data, int statusCode = 200) + => new() { IsSuccess = true, Data = data, StatusCode = statusCode }; + + public static ApiResponse Failure(int statusCode, string? error = null) + => new() { IsSuccess = false, StatusCode = statusCode, ErrorMessage = error }; } + +/// +/// Response ohne Daten (für POST/PUT/DELETE die nur Status zurückgeben). +/// +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 }; +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/Base/ApiServiceBase.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/Base/ApiServiceBase.cs index deaf3702..2ec26168 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/Base/ApiServiceBase.cs +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/Base/ApiServiceBase.cs @@ -1,6 +1,110 @@ -namespace EnvelopeGenerator.ReceiverUI.Client.Services.Base +using System.Net.Http.Json; +using Microsoft.Extensions.Logging; + +namespace EnvelopeGenerator.ReceiverUI.Client.Services.Base; + +/// +/// 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 +/// +public abstract class ApiServiceBase { - public class ApiServiceBase + protected readonly HttpClient Http; + protected readonly ILogger Logger; + + protected ApiServiceBase(HttpClient http, ILogger logger) { + Http = http; + Logger = logger; } -} + + /// + /// GET-Request mit Deserialisierung. + /// Alle API GET-Aufrufe gehen durch diese Methode. + /// + protected async Task> GetAsync(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.Failure((int)response.StatusCode, errorBody); + } + + var data = await response.Content.ReadFromJsonAsync(cancellationToken: ct); + return ApiResponse.Success(data!, (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) + { + Logger.LogWarning("GET {Endpoint} was cancelled", endpoint); + return ApiResponse.Failure(0, "Anfrage abgebrochen."); + } + } + + /// + /// POST-Request mit Body und Response-Deserialisierung. + /// + protected async Task> PostAsync( + 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.Failure((int)response.StatusCode, errorBody); + } + + var data = await response.Content.ReadFromJsonAsync(cancellationToken: ct); + return ApiResponse.Success(data!, (int)response.StatusCode); + } + catch (HttpRequestException ex) + { + Logger.LogError(ex, "HTTP error calling POST {Endpoint}", endpoint); + return ApiResponse.Failure(0, "Verbindung zum Server fehlgeschlagen."); + } + } + + /// + /// POST-Request ohne Response-Body (z.B. Logout). + /// + protected async Task PostAsync( + 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."); + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/EnvelopeService.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/EnvelopeService.cs index e453f3eb..de93f606 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/EnvelopeService.cs +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/EnvelopeService.cs @@ -1,6 +1,16 @@ -namespace EnvelopeGenerator.ReceiverUI.Client.Services +using EnvelopeGenerator.ReceiverUI.Client.Models; +using EnvelopeGenerator.ReceiverUI.Client.Services.Base; + +namespace EnvelopeGenerator.ReceiverUI.Client.Services; + +public class EnvelopeService : ApiServiceBase, IEnvelopeService { - public class EnvelopeService - { - } -} + public EnvelopeService(HttpClient http, ILogger logger) : base(http, logger) { } + + public Task>> GetEnvelopesAsync(CancellationToken ct = default) + => GetAsync>("api/envelope", ct); + + public Task>> GetEnvelopeReceiversAsync( + CancellationToken ct = default) + => GetAsync>("api/envelopereceiver", ct); +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/IAuthService.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/IAuthService.cs index 8cf3f6e6..f9061b85 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/IAuthService.cs +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/IAuthService.cs @@ -1,6 +1,20 @@ -namespace EnvelopeGenerator.ReceiverUI.Client.Services +using EnvelopeGenerator.ReceiverUI.Client.Services.Base; + +namespace EnvelopeGenerator.ReceiverUI.Client.Services; + +/// +/// 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 +/// +public interface IAuthService { - public interface IAuthService - { - } -} + /// Prüft ob der Nutzer eingeloggt ist → GET /api/auth/check + Task CheckAuthAsync(string? role = null, CancellationToken ct = default); + + /// Logout → POST /api/auth/logout + Task LogoutAsync(CancellationToken ct = default); +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/IEnvelopeService.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/IEnvelopeService.cs index b89fc6c0..80b1b4a3 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/IEnvelopeService.cs +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/IEnvelopeService.cs @@ -1,6 +1,18 @@ -namespace EnvelopeGenerator.ReceiverUI.Client.Services +using EnvelopeGenerator.ReceiverUI.Client.Models; +using EnvelopeGenerator.ReceiverUI.Client.Services.Base; + +namespace EnvelopeGenerator.ReceiverUI.Client.Services; + +/// +/// Kommuniziert mit EnvelopeController und EnvelopeReceiverController. +/// Verwendet Client-eigene Models statt der Server-DTOs. +/// +public interface IEnvelopeService { - public interface IEnvelopeService - { - } -} + /// Lädt Umschläge → GET /api/envelope + Task>> GetEnvelopesAsync(CancellationToken ct = default); + + /// Lädt EnvelopeReceiver → GET /api/envelopereceiver + Task>> GetEnvelopeReceiversAsync( + CancellationToken ct = default); +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/State/EnvelopeState.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/State/EnvelopeState.cs index 6d23726f..e11a9387 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/State/EnvelopeState.cs +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/State/EnvelopeState.cs @@ -1,6 +1,81 @@ -namespace EnvelopeGenerator.ReceiverUI.Client.State +namespace EnvelopeGenerator.ReceiverUI.Client.State; + +/// +/// 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. +/// +public class EnvelopeState { - public class EnvelopeState + private EnvelopePageStatus _status = EnvelopePageStatus.Loading; + private string? _errorMessage; + + /// Aktueller Seitenstatus + public EnvelopePageStatus Status { + get => _status; + private set + { + _status = value; + NotifyStateChanged(); + } } + + /// Fehlermeldung (falls vorhanden) + 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(); } + +/// Alle möglichen Zustände der Umschlag-Seite +public enum EnvelopePageStatus +{ + Loading, + RequiresAccessCode, + RequiresTwoFactor, + ShowDocument, + AlreadySigned, + Rejected, + NotFound, + Expired, + Error +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/_Imports.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/_Imports.razor index 8397160c..85574bc1 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/_Imports.razor +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/_Imports.razor @@ -1,5 +1,6 @@ @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 @@ -7,3 +8,10 @@ @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 \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/App.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/App.razor index 5879f1dd..f45eb9cb 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/App.razor +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/App.razor @@ -1,5 +1,5 @@  - + @@ -17,4 +17,4 @@ - + \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Layout/MainLayout.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Layout/MainLayout.razor index 5a24bb13..e28f2bfb 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Layout/MainLayout.razor +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Layout/MainLayout.razor @@ -1,23 +1,34 @@ @inherits LayoutComponentBase -
- - -
-
- About +
+
+
+ signFLOW
+
-
- @Body -
+
+ + + @Body + + +
+

😵 Ein unerwarteter Fehler ist aufgetreten

+

Bitte versuchen Sie es erneut.

+ +
+
+
+ +
+ © @DateTime.Now.Year Digital Data GmbH +
-
- An unhandled error has occurred. - Reload - 🗙 -
+@code { + private ErrorBoundary? _errorBoundary; + + private void Recover() => _errorBoundary?.Recover(); +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Routes.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Routes.razor index d39c7e89..a2f43181 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Routes.razor +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Routes.razor @@ -3,4 +3,12 @@ - + + +
+

404

+

Diese Seite wurde nicht gefunden.

+
+
+
+ \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/_Imports.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/_Imports.razor new file mode 100644 index 00000000..c8cadf00 --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/_Imports.razor @@ -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 \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.csproj b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.csproj index e4d216dc..440ffc1e 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.csproj +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.csproj @@ -1,18 +1,15 @@ - - net8.0 - enable - enable - + + net9.0 + enable + enable + - - - - + + + + + - - - - - + \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Program.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Program.cs index 119ae224..85c40e23 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Program.cs +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Program.cs @@ -1,16 +1,21 @@ -using EnvelopeGenerator.ReceiverUI.Client.Pages; -using EnvelopeGenerator.ReceiverUI.Components; +using EnvelopeGenerator.ReceiverUI.Components; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. 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(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseWebAssemblyDebugging(); @@ -18,18 +23,20 @@ if (app.Environment.IsDevelopment()) else { app.UseExceptionHandler("/Error", createScopeForErrors: true); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 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() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(EnvelopeGenerator.ReceiverUI.Client._Imports).Assembly); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/appsettings.json b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/appsettings.json index 10f68b8c..bbee3a49 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/appsettings.json +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/appsettings.json @@ -1,9 +1,9 @@ { + "ApiBaseUrl": "https://localhost:5001", "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } - }, - "AllowedHosts": "*" -} + } +} \ No newline at end of file