Add login route and enhance login page functionality
Added a new `/login/{**catch-all}` route in `yarp.json` for the `receiver-ui` cluster. Updated `Login.razor` with a modernized UI, including a gradient header, improved error handling, and a toggle for password visibility. Integrated `AuthService` for API-based login validation.
Refactored `Login.razor` logic to handle login outcomes (`Success`, `InvalidCode`, `NotFound`, `Error`) using the new `EnvelopeLoginResult` enum. Added `LoginEnvelopeReceiverAsync` method in `AuthService` to handle login requests via `POST /api/Auth/envelope-receiver/{key}`. Improved code maintainability and user experience.
This commit is contained in:
@@ -9,6 +9,14 @@
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
}
|
||||
},
|
||||
"receiver-ui-login": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 100,
|
||||
"Match": {
|
||||
"Path": "/login/{**catch-all}",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
}
|
||||
},
|
||||
"receiver-ui-sender": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 100,
|
||||
|
||||
@@ -1,44 +1,125 @@
|
||||
@page "/login/{EnvelopeKey}"
|
||||
@using EnvelopeGenerator.ReceiverUI.Options
|
||||
@using Microsoft.Extensions.Options
|
||||
@using EnvelopeGenerator.ReceiverUI.Services
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
|
||||
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
||||
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
||||
|
||||
<div class="card-header text-white text-center py-4 border-0" style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;">
|
||||
<div class="mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h5 class="mb-0 fw-semibold">Dokument öffnen</h5>
|
||||
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang mit Zugangscode</p>
|
||||
</div>
|
||||
|
||||
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100 bg-light">
|
||||
<div class="card shadow-sm" style="max-width: 420px; width: 100%;">
|
||||
<div class="card-body p-4">
|
||||
|
||||
<h4 class="card-title mb-1">Zugang zum Dokument</h4>
|
||||
<p class="text-muted mb-4" style="font-size: 0.9rem;">
|
||||
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument zu öffnen.
|
||||
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
|
||||
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
|
||||
</p>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(ErrorMessage)) {
|
||||
<div class="alert alert-danger py-2">@ErrorMessage</div>
|
||||
@if (LoginResult == EnvelopeLoginResult.NotFound) {
|
||||
<div class="alert alert-warning d-flex align-items-start gap-2 py-2" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong>Dokument nicht gefunden.</strong><br />
|
||||
<span style="font-size:0.85rem;">Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</span>
|
||||
</div>
|
||||
</div>
|
||||
} else if (LoginResult == EnvelopeLoginResult.InvalidCode) {
|
||||
<div class="alert alert-danger d-flex align-items-start gap-2 py-2" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong>Ungültiger Zugangscode.</strong><br />
|
||||
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
||||
</div>
|
||||
</div>
|
||||
} else if (LoginResult == EnvelopeLoginResult.Error) {
|
||||
<div class="alert alert-secondary d-flex align-items-start gap-2 py-2" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong>Serverfehler.</strong><br />
|
||||
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="login-access-code">Zugangscode</label>
|
||||
<input id="login-access-code"
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="Zugangscode eingeben"
|
||||
@bind="AccessCode"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="OnKeyDownAsync"
|
||||
disabled="@IsLoading" />
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-medium" for="login-access-code">
|
||||
Zugangscode
|
||||
<span class="text-danger ms-1">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" viewBox="0 0 16 16">
|
||||
<path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1.837 1.337A3.5 3.5 0 0 1 3.5 11.5zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input id="login-access-code"
|
||||
type="@(ShowCode ? "text" : "password")"
|
||||
class="form-control border-start-0 border-end-0 @(LoginResult == EnvelopeLoginResult.InvalidCode ? "is-invalid" : null)"
|
||||
placeholder="Zugangscode eingeben"
|
||||
@bind="AccessCode"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="OnKeyDownAsync"
|
||||
disabled="@IsLoading"
|
||||
autocomplete="one-time-code" />
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary border-start-0"
|
||||
style="border-left: none;"
|
||||
tabindex="-1"
|
||||
@onclick="() => ShowCode = !ShowCode">
|
||||
@if (ShowCode) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
|
||||
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
|
||||
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z"/>
|
||||
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z"/>
|
||||
</svg>
|
||||
} else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
||||
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100"
|
||||
<button class="btn btn-primary w-100 py-2 fw-medium"
|
||||
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
||||
@onclick="SubmitAsync"
|
||||
disabled="@(IsLoading || string.IsNullOrWhiteSpace(AccessCode))">
|
||||
@if (IsLoading) {
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
<span>Überprüfen …</span>
|
||||
} else {
|
||||
<span>Weiter</span>
|
||||
<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"/>
|
||||
</svg>
|
||||
<span>Dokument öffnen</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-center text-muted py-3 border-0 bg-transparent" style="font-size: 0.78rem;">
|
||||
Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,8 +127,9 @@
|
||||
[Parameter] public string EnvelopeKey { get; set; } = string.Empty;
|
||||
|
||||
string AccessCode = string.Empty;
|
||||
string? ErrorMessage;
|
||||
bool ShowCode;
|
||||
bool IsLoading;
|
||||
EnvelopeLoginResult? LoginResult;
|
||||
|
||||
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
|
||||
if (e.Key == "Enter")
|
||||
@@ -55,17 +137,22 @@
|
||||
}
|
||||
|
||||
async Task SubmitAsync() {
|
||||
if (string.IsNullOrWhiteSpace(AccessCode)) return;
|
||||
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
|
||||
|
||||
IsLoading = true;
|
||||
ErrorMessage = null;
|
||||
LoginResult = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
// TODO: API entegrasyonu buraya eklenecek
|
||||
await Task.Delay(500); // placeholder
|
||||
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
||||
|
||||
if (result == EnvelopeLoginResult.Success) {
|
||||
Navigation.NavigateTo($"/receiver/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
|
||||
LoginResult = result;
|
||||
IsLoading = false;
|
||||
ErrorMessage = "Der eingegebene Zugangscode ist ungültig. Bitte versuchen Sie es erneut.";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
||||
|
||||
public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private readonly ApiOptions _api = apiOptions.Value;
|
||||
@@ -17,4 +19,27 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
||||
return response.StatusCode == HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits the access code for the given envelope key.
|
||||
/// Calls POST /api/Auth/envelope-receiver/{key} with multipart/form-data.
|
||||
/// On success the API sets an authentication cookie automatically.
|
||||
/// </summary>
|
||||
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default)
|
||||
{
|
||||
var form = new MultipartFormDataContent();
|
||||
form.Add(new StringContent(accessCode), "AccessCode");
|
||||
|
||||
var response = await http.PostAsync(
|
||||
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
|
||||
form, cancel);
|
||||
|
||||
return response.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.OK => EnvelopeLoginResult.Success,
|
||||
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
|
||||
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
|
||||
_ => EnvelopeLoginResult.Error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user