Initial commit: Lagerbestand Blazor Projekt
This commit is contained in:
commit
cba9414e92
105 files changed
+36431
No files matched your search
@@ -0,0 +1,398 @@
|
||||
@page "/login"
|
||||
@layout EmptyLayout
|
||||
@inject IAuthService AuthService
|
||||
@inject ITwoFactorService TwoFactorService
|
||||
@inject ISiteSettingsService SiteSettingsService
|
||||
@inject UserSessionService UserSession
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<PageTitle>Login - Reifen Späth Lagerverwaltung</PageTitle>
|
||||
|
||||
<SeoMetaTags
|
||||
Title="Login - Reifen Späth Lagerverwaltung"
|
||||
Description="Anmeldung zur Reifen Späth Lagerverwaltung. Zugang für Mitarbeiter und Administratoren."
|
||||
Robots="noindex, nofollow"
|
||||
CanonicalUrl="https://www.reifenspaeth.de/login" />
|
||||
|
||||
<div class="login-page">
|
||||
<main class="login-container" role="main">
|
||||
<header class="login-header">
|
||||
<h1 class="login-brand">REIFEN SPÄTH</h1>
|
||||
<p class="login-subtitle">Lagerverwaltung</p>
|
||||
</header>
|
||||
|
||||
<section class="login-card" aria-labelledby="login-title">
|
||||
@if (!showTwoFactorStep)
|
||||
{
|
||||
<h2 id="login-title" class="login-title">Anmelden</h2>
|
||||
|
||||
<form @onsubmit="HandleLogin" @onsubmit:preventDefault>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="username">Benutzername</label>
|
||||
<input type="text"
|
||||
id="username"
|
||||
class="form-control @(!string.IsNullOrEmpty(errorMessage) ? "is-invalid" : "")"
|
||||
@bind="username"
|
||||
@bind:event="oninput"
|
||||
@onfocus="ClearError"
|
||||
placeholder="Benutzername eingeben"
|
||||
autocomplete="username"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
disabled="@isLoading"
|
||||
maxlength="50"
|
||||
required
|
||||
aria-required="true"
|
||||
aria-invalid="@(!string.IsNullOrEmpty(errorMessage))"
|
||||
aria-describedby="@(!string.IsNullOrEmpty(errorMessage) ? "login-error" : null)" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Passwort</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
class="form-control @(!string.IsNullOrEmpty(errorMessage) ? "is-invalid" : "")"
|
||||
@bind="password"
|
||||
@bind:event="oninput"
|
||||
@onfocus="ClearError"
|
||||
placeholder="Passwort eingeben"
|
||||
autocomplete="current-password"
|
||||
disabled="@isLoading"
|
||||
required
|
||||
aria-required="true"
|
||||
aria-invalid="@(!string.IsNullOrEmpty(errorMessage))"
|
||||
aria-describedby="@(!string.IsNullOrEmpty(errorMessage) ? "login-error" : null)" />
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div id="login-error" class="alert alert-danger" role="alert" aria-live="assertive">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
<span>@errorMessage</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button type="submit"
|
||||
class="btn btn-login"
|
||||
disabled="@(isLoading || !CanLogin)"
|
||||
aria-busy="@isLoading">
|
||||
@if (isLoading)
|
||||
{
|
||||
<span class="spinner-sm" aria-hidden="true"></span>
|
||||
<span>Anmelden...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Anmelden</span>
|
||||
}
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* 2FA Code Entry Step *@
|
||||
<h2 id="login-title" class="login-title">Zwei-Faktor-Authentifizierung</h2>
|
||||
<p class="tfa-description">Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein.</p>
|
||||
|
||||
<form @onsubmit="HandleTwoFactorSubmit" @onsubmit:preventDefault>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="tfa-code">Authentifizierungscode</label>
|
||||
<input type="text"
|
||||
id="tfa-code"
|
||||
class="form-control tfa-code-input @(!string.IsNullOrEmpty(errorMessage) ? "is-invalid" : "")"
|
||||
@bind="twoFactorCode"
|
||||
@bind:event="oninput"
|
||||
@onfocus="ClearError"
|
||||
placeholder="000000"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]{6}"
|
||||
maxlength="6"
|
||||
disabled="@isLoading"
|
||||
required
|
||||
aria-required="true" />
|
||||
</div>
|
||||
|
||||
@if (emailFallbackAvailable)
|
||||
{
|
||||
<div class="tfa-fallback">
|
||||
<button type="button" class="btn-link" @onclick="SendEmailFallbackCode" disabled="@isSendingEmail">
|
||||
@if (isSendingEmail)
|
||||
{
|
||||
<span class="spinner-sm" aria-hidden="true"></span>
|
||||
}
|
||||
Code per E-Mail senden
|
||||
</button>
|
||||
@if (!string.IsNullOrEmpty(emailFallbackMessage))
|
||||
{
|
||||
<span class="tfa-email-status @emailFallbackClass">@emailFallbackMessage</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div id="login-error" class="alert alert-danger" role="alert" aria-live="assertive">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
<span>@errorMessage</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button type="submit"
|
||||
class="btn btn-login"
|
||||
disabled="@(isLoading || twoFactorCode.Length != 6)"
|
||||
aria-busy="@isLoading">
|
||||
@if (isLoading)
|
||||
{
|
||||
<span class="spinner-sm" aria-hidden="true"></span>
|
||||
<span>Überprüfen...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Bestätigen</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-back" @onclick="BackToLogin">
|
||||
? Zurück
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
<footer class="login-footer">
|
||||
<a href="/" class="back-link">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false">
|
||||
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||
<polyline points="12 19 5 12 12 5"></polyline>
|
||||
</svg>
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string username = "";
|
||||
private string password = "";
|
||||
private bool isLoading;
|
||||
private string? errorMessage;
|
||||
|
||||
// 2FA state
|
||||
private bool showTwoFactorStep;
|
||||
private string twoFactorCode = "";
|
||||
private AppUser? pendingUser;
|
||||
private bool emailFallbackAvailable;
|
||||
private bool isSendingEmail;
|
||||
private string? emailFallbackMessage;
|
||||
private string emailFallbackClass = "";
|
||||
private string? pendingEmailCode;
|
||||
private DateTime? emailCodeExpiry;
|
||||
|
||||
private bool CanLogin =>
|
||||
!string.IsNullOrWhiteSpace(username) &&
|
||||
!string.IsNullOrWhiteSpace(password);
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (UserSession.IsAuthenticated)
|
||||
{
|
||||
Navigation.NavigateTo("/tire-storage");
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearError()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
errorMessage = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
if (isLoading || !CanLogin) return;
|
||||
|
||||
errorMessage = null;
|
||||
isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(250);
|
||||
|
||||
var user = await AuthService.LoginAsync(username.Trim(), password);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
// Check if 2FA is required
|
||||
var tfSettings = await SiteSettingsService.GetTwoFactorSettingsAsync();
|
||||
if (tfSettings.IsEnabled && user.TwoFactorEnabled && !string.IsNullOrEmpty(user.TwoFactorSecret))
|
||||
{
|
||||
pendingUser = user;
|
||||
showTwoFactorStep = true;
|
||||
twoFactorCode = "";
|
||||
emailFallbackAvailable = tfSettings.EmailFallbackEnabled;
|
||||
emailFallbackMessage = null;
|
||||
pendingEmailCode = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
await CompleteLogin(user);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Ungültiger Benutzername oder Passwort.";
|
||||
password = "";
|
||||
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("qrScanner.playSound", "error");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
errorMessage = "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleTwoFactorSubmit()
|
||||
{
|
||||
if (isLoading || pendingUser == null || twoFactorCode.Length != 6) return;
|
||||
|
||||
errorMessage = null;
|
||||
isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(200);
|
||||
bool valid = false;
|
||||
|
||||
// Check email fallback code first
|
||||
if (pendingEmailCode != null && emailCodeExpiry.HasValue && DateTime.UtcNow < emailCodeExpiry.Value)
|
||||
{
|
||||
if (System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
|
||||
System.Text.Encoding.UTF8.GetBytes(pendingEmailCode),
|
||||
System.Text.Encoding.UTF8.GetBytes(twoFactorCode)))
|
||||
{
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check TOTP code
|
||||
if (!valid && !string.IsNullOrEmpty(pendingUser.TwoFactorSecret))
|
||||
{
|
||||
valid = TwoFactorService.ValidateTotpCode(pendingUser.TwoFactorSecret, twoFactorCode);
|
||||
}
|
||||
|
||||
if (valid)
|
||||
{
|
||||
await CompleteLogin(pendingUser);
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Ungültiger Code. Bitte versuchen Sie es erneut.";
|
||||
twoFactorCode = "";
|
||||
try { await JS.InvokeVoidAsync("qrScanner.playSound", "error"); } catch { }
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
errorMessage = "Ein Fehler ist aufgetreten.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendEmailFallbackCode()
|
||||
{
|
||||
if (pendingUser == null || isSendingEmail) return;
|
||||
|
||||
isSendingEmail = true;
|
||||
emailFallbackMessage = null;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var smtpSettings = await SiteSettingsService.GetSmtpSettingsAsync();
|
||||
if (!smtpSettings.IsEnabled || !smtpSettings.IsConfigured)
|
||||
{
|
||||
emailFallbackMessage = "E-Mail-Versand nicht konfiguriert.";
|
||||
emailFallbackClass = "text-danger";
|
||||
return;
|
||||
}
|
||||
|
||||
var code = TwoFactorService.GenerateEmailCode();
|
||||
var sent = await TwoFactorService.SendEmailCodeAsync(smtpSettings.FromEmail, code, pendingUser.Username);
|
||||
if (sent)
|
||||
{
|
||||
pendingEmailCode = code;
|
||||
emailCodeExpiry = DateTime.UtcNow.AddMinutes(5);
|
||||
emailFallbackMessage = "Code wurde gesendet.";
|
||||
emailFallbackClass = "text-success";
|
||||
}
|
||||
else
|
||||
{
|
||||
emailFallbackMessage = "Fehler beim E-Mail-Versand.";
|
||||
emailFallbackClass = "text-danger";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
emailFallbackMessage = "Fehler beim E-Mail-Versand.";
|
||||
emailFallbackClass = "text-danger";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSendingEmail = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CompleteLogin(AppUser user)
|
||||
{
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("localStorage.setItem", "userId", user.Id.ToString());
|
||||
}
|
||||
catch { }
|
||||
|
||||
UserSession.SetUser(user);
|
||||
Navigation.NavigateTo("/tire-storage");
|
||||
}
|
||||
|
||||
private void BackToLogin()
|
||||
{
|
||||
showTwoFactorStep = false;
|
||||
twoFactorCode = "";
|
||||
pendingUser = null;
|
||||
errorMessage = null;
|
||||
password = "";
|
||||
pendingEmailCode = null;
|
||||
emailCodeExpiry = null;
|
||||
emailFallbackMessage = null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user