Initial commit: Lagerbestand Blazor Projekt

This commit is contained in:
spaethf committed 2026-09-21 21:28:23 +02:00
commit cba9414e92
105 files changed
+36431

No files matched your search

+983
View File
@@ -0,0 +1,983 @@
@page "/users"
@implements IAsyncDisposable
@inject IAuthService AuthService
@inject ITwoFactorService TwoFactorService
@inject ISiteSettingsService SiteSettingsService
@inject UserSessionService UserSession
@inject NavigationManager Navigation
@inject IJSRuntime JS
@using Lagerbestand.Components.Shared
<PageTitle>Benutzerverwaltung - Reifen Späth Lagerverwaltung</PageTitle>
<SeoMetaTags
Title="Benutzerverwaltung - Reifen Späth Lagerverwaltung"
Description="Verwaltung der Benutzerkonten in der Reifen Späth Lagerverwaltung."
Robots="noindex, nofollow" />
@if (!UserSession.IsAdmin)
{
<section class="access-denied" role="alert" aria-labelledby="access-denied-title">
<div class="access-denied-icon" aria-hidden="true">??</div>
<h1 id="access-denied-title">Zugriff verweigert</h1>
<p>Sie haben keine Berechtigung für diese Seite.</p>
<a href="/tire-storage" class="btn btn-primary">Zur Lagerübersicht</a>
</section>
}
else
{
<main class="users-container" role="main">
<header class="header-row">
<div class="header-info">
<h1>Benutzerverwaltung</h1>
<p class="header-subtitle">@users.Count Benutzer insgesamt · @users.Count(u => u.IsActive) aktiv</p>
</div>
<div class="header-actions">
<button class="btn btn-outline-primary btn-sm" @onclick="LoadUsers" disabled="@isLoading" title="Aktualisieren">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6"></path>
<path d="M1 20v-6h6"></path>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
</button>
<button class="btn btn-primary" @onclick="OpenCreateUserModal">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Neuer Benutzer
</button>
</div>
</header>
@if (isLoading)
{
<div class="loading-spinner">
<div class="spinner-border" role="status"></div>
<p>Lade Benutzer...</p>
</div>
}
else if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger">
<span>@errorMessage</span>
<button type="button" class="alert-dismiss" @onclick="() => errorMessage = null">×</button>
</div>
}
else
{
@if (!string.IsNullOrEmpty(successMessage))
{
<div class="alert alert-success">
<span>@successMessage</span>
<button type="button" class="alert-dismiss" @onclick="() => successMessage = null">×</button>
</div>
}
<div class="users-table">
<div class="table-header">
<span class="col-username">Benutzername</span>
<span class="col-displayname">Anzeigename</span>
<span class="col-role">Rolle</span>
<span class="col-2fa">2FA</span>
<span class="col-status">Status</span>
<span class="col-lastlogin">Letzter Login</span>
<span class="col-actions">Aktionen</span>
</div>
<div class="table-body">
@foreach (var user in users)
{
var isCurrentUser = user.Id == UserSession.CurrentUser?.Id;
<div class="table-row @(user.IsActive ? "" : "inactive") @(isCurrentUser ? "current-user" : "")">
<span class="col-username">
@user.Username
@if (isCurrentUser)
{
<span class="current-user-badge">Sie</span>
}
</span>
<span class="col-displayname">@(user.DisplayName ?? "-")</span>
<span class="col-role">
<span class="role-badge @GetRoleClass(user.Role)">@GetRoleName(user.Role)</span>
</span>
<span class="col-2fa">
@if (twoFactorGlobalEnabled)
{
@if (user.TwoFactorEnabled)
{
<span class="tfa-badge tfa-on" title="@(user.TwoFactorEnforced ? "2FA aktiv (erzwungen)" : "2FA aktiv")">
?? @(user.TwoFactorEnforced ? "Erzwungen" : "Aktiv")
</span>
}
else
{
<span class="tfa-badge tfa-off" title="2FA nicht eingerichtet">Aus</span>
}
}
else
{
<span class="text-muted">—</span>
}
</span>
<span class="col-status">
<span class="status-badge @(user.IsActive ? "active" : "inactive")">
@(user.IsActive ? "Aktiv" : "Inaktiv")
</span>
</span>
<span class="col-lastlogin">
@if (user.LastLoginAt.HasValue)
{
<span title="@user.LastLoginAt.Value.ToLocalTime().ToString("dd.MM.yyyy HH:mm:ss")">
@FormatLastLogin(user.LastLoginAt.Value)
</span>
}
else
{
<span class="text-muted">Nie</span>
}
</span>
<span class="col-actions">
<button class="btn-icon-sm" @onclick="() => OpenEditUserModal(user)" title="Bearbeiten">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button class="btn-icon-sm" @onclick="() => OpenPasswordModal(user)" title="Passwort ändern">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</button>
@if (twoFactorGlobalEnabled)
{
@if (user.TwoFactorEnabled)
{
<button class="btn-icon-sm warning" @onclick="() => Reset2Fa(user)" title="2FA zurücksetzen">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
</button>
}
@if (!user.TwoFactorEnforced)
{
<button class="btn-icon-sm" @onclick="() => Enforce2Fa(user, true)" title="2FA erzwingen">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
</button>
}
else
{
<button class="btn-icon-sm" @onclick="() => Enforce2Fa(user, false)" title="2FA-Zwang aufheben">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke-dasharray="4 4"></path>
</svg>
</button>
}
}
@if (!isCurrentUser)
{
<button class="btn-icon-sm danger" @onclick="() => OpenDeleteModal(user)" title="Löschen">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
}
</span>
</div>
}
</div>
</div>
@if (users.Count == 0)
{
<div class="empty-state">
<div class="empty-icon">??</div>
<h3>Keine Benutzer gefunden</h3>
<p>Erstellen Sie einen neuen Benutzer, um zu beginnen.</p>
</div>
}
}
</main>
@* Create/Edit User Modal *@
@if (showUserModal)
{
<div class="modal-backdrop" @onclick="CloseUserModal"></div>
<div class="app-modal" @onclick="CloseUserModal" role="dialog" aria-modal="true">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(isEditingUser ? "Benutzer bearbeiten" : "Neuer Benutzer")</h5>
<button type="button" class="btn-close" @onclick="CloseUserModal" disabled="@isSaving"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Benutzername <span class="required">*</span></label>
<input type="text"
class="form-control @(usernameError != null ? "is-invalid" : "")"
@bind="editUsername"
@bind:event="oninput"
@onfocusout="ValidateUsernameField"
placeholder="Benutzername eingeben"
disabled="@isSaving"
maxlength="50" />
@if (usernameError != null)
{
<div class="invalid-feedback">@usernameError</div>
}
<small class="form-text">3-50 Zeichen, nur Buchstaben, Zahlen, Punkte, Unterstriche und Bindestriche</small>
</div>
@if (!isEditingUser)
{
<div class="mb-3">
<label class="form-label">Passwort <span class="required">*</span></label>
<input type="password"
class="form-control @(passwordError != null ? "is-invalid" : "")"
@bind="newPassword"
@bind:event="oninput"
@onfocusout="ValidatePasswordField"
placeholder="Passwort eingeben"
disabled="@isSaving" />
@if (passwordError != null)
{
<div class="invalid-feedback">@passwordError</div>
}
<small class="form-text">Mindestens 4 Zeichen</small>
</div>
}
<div class="mb-3">
<label class="form-label">Anzeigename</label>
<input type="text"
class="form-control"
@bind="editDisplayName"
placeholder="Optionaler Anzeigename"
disabled="@isSaving"
maxlength="200" />
<small class="form-text">Wird in der Oberfläche angezeigt, falls gesetzt</small>
</div>
<div class="mb-3">
<label class="form-label">Rolle <span class="required">*</span></label>
<select class="form-select" @bind="editRole" disabled="@isSaving">
<option value="@UserRole.User">Benutzer - Nur Anzeige</option>
<option value="@UserRole.Worker">Mitarbeiter - Lager, Einlagern, Abholen</option>
<option value="@UserRole.Admin">Administrator - Volle Kontrolle</option>
</select>
@if (isEditingUser && editUserId == UserSession.CurrentUser?.Id && editRole != UserRole.Admin)
{
<div class="alert alert-warning mt-2 mb-0">
<small>Achtung: Sie ändern Ihre eigene Rolle. Sie verlieren möglicherweise Admin-Rechte.</small>
</div>
}
</div>
@if (isEditingUser)
{
<div class="mb-3">
<label class="form-check">
<input type="checkbox" class="form-check-input" @bind="editIsActive" disabled="@isSaving" />
<span class="form-check-label">Benutzer ist aktiv</span>
</label>
@if (!editIsActive)
{
<small class="form-text text-warning">Inaktive Benutzer können sich nicht anmelden.</small>
}
</div>
}
@if (!string.IsNullOrEmpty(modalError))
{
<div class="alert alert-danger">@modalError</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseUserModal" disabled="@isSaving">
Abbrechen
</button>
<button type="button" class="btn btn-primary" @onclick="SaveUser" disabled="@isSaving">
@if (isSaving)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
@(isEditingUser ? "Speichern" : "Erstellen")
</button>
</div>
</div>
</div>
</div>
}
@* Password Modal *@
@if (showPasswordModal)
{
<div class="modal-backdrop" @onclick="ClosePasswordModal"></div>
<div class="app-modal" @onclick="ClosePasswordModal" role="dialog" aria-modal="true">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Passwort ändern</h5>
<button type="button" class="btn-close" @onclick="ClosePasswordModal" disabled="@isSaving"></button>
</div>
<div class="modal-body">
<div class="user-info-box mb-3">
<span class="label">Benutzer:</span>
<span class="value">@passwordUser?.Username</span>
</div>
<div class="mb-3">
<label class="form-label">Neues Passwort <span class="required">*</span></label>
<input type="password"
class="form-control"
@bind="newPassword"
@bind:event="oninput"
placeholder="Neues Passwort eingeben"
disabled="@isSaving" />
</div>
<div class="mb-3">
<label class="form-label">Passwort bestätigen <span class="required">*</span></label>
<input type="password"
class="form-control @(newPassword != confirmPassword && !string.IsNullOrEmpty(confirmPassword) ? "is-invalid" : "")"
@bind="confirmPassword"
@bind:event="oninput"
placeholder="Passwort wiederholen"
disabled="@isSaving" />
@if (newPassword != confirmPassword && !string.IsNullOrEmpty(confirmPassword))
{
<div class="invalid-feedback">Passwörter stimmen nicht überein</div>
}
</div>
@if (!string.IsNullOrEmpty(modalError))
{
<div class="alert alert-danger">@modalError</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="ClosePasswordModal" disabled="@isSaving">
Abbrechen
</button>
<button type="button" class="btn btn-primary" @onclick="ChangePassword" disabled="@(isSaving || newPassword != confirmPassword)">
@if (isSaving)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
Passwort ändern
</button>
</div>
</div>
</div>
</div>
}
@* Delete Confirmation Modal *@
@if (showDeleteModal)
{
<div class="modal-backdrop" @onclick="CloseDeleteModal"></div>
<div class="app-modal" @onclick="CloseDeleteModal" role="dialog" aria-modal="true">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Benutzer löschen</h5>
<button type="button" class="btn-close" @onclick="CloseDeleteModal" disabled="@isSaving"></button>
</div>
<div class="modal-body">
<div class="delete-warning">
<div class="warning-icon">??</div>
<p class="warning-text">
Möchten Sie den Benutzer <strong>@deleteUser?.Username</strong> wirklich löschen?
</p>
<p class="warning-subtext">
Diese Aktion kann nicht rückgängig gemacht werden.
</p>
</div>
@if (!string.IsNullOrEmpty(modalError))
{
<div class="alert alert-danger">@modalError</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseDeleteModal" disabled="@isSaving">
Abbrechen
</button>
<button type="button" class="btn btn-danger" @onclick="DeleteUser" disabled="@isSaving">
@if (isSaving)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
Endgültig löschen
</button>
</div>
</div>
</div>
</div>
}
@* 2FA Setup Modal *@
@if (show2FaSetupModal)
{
<div class="modal-backdrop" @onclick="Close2FaSetupModal"></div>
<div class="app-modal" @onclick="Close2FaSetupModal" role="dialog" aria-modal="true">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">2FA einrichten – @setup2FaUser?.Username</h5>
<button type="button" class="btn-close" @onclick="Close2FaSetupModal" disabled="@isSaving"></button>
</div>
<div class="modal-body">
@if (!string.IsNullOrEmpty(setup2FaQrDataUrl))
{
<div class="tfa-setup-instructions">
<p>Scannen Sie den QR-Code mit einer Authenticator-App (z.B. Google Authenticator, Microsoft Authenticator, Authy):</p>
</div>
<div class="tfa-qr-container">
<img src="@setup2FaQrDataUrl" alt="2FA QR-Code" class="tfa-qr-image" />
</div>
<div class="tfa-manual-key">
<span class="label">Manueller Schlüssel:</span>
<code class="secret-key">@setup2FaSecret</code>
</div>
<div class="tfa-confirm-section">
<label class="form-label">Bestätigungscode eingeben <span class="required">*</span></label>
<input type="text"
class="form-control tfa-confirm-input"
@bind="setup2FaCode"
@bind:event="oninput"
placeholder="6-stelliger Code"
maxlength="6"
inputmode="numeric"
disabled="@isSaving" />
<small class="form-text">Geben Sie den Code aus Ihrer Authenticator-App ein, um die Einrichtung abzuschließen.</small>
</div>
}
@if (!string.IsNullOrEmpty(modalError))
{
<div class="alert alert-danger">@modalError</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="Close2FaSetupModal" disabled="@isSaving">
Abbrechen
</button>
<button type="button" class="btn btn-primary" @onclick="Confirm2FaSetup" disabled="@(isSaving || string.IsNullOrWhiteSpace(setup2FaCode) || setup2FaCode?.Length != 6)">
@if (isSaving)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
Bestätigen & Aktivieren
</button>
</div>
</div>
</div>
</div>
}
}
@code {
private List<AppUser> users = new();
private bool isLoading = true;
private string? errorMessage;
private string? successMessage;
private bool twoFactorGlobalEnabled;
// User Modal
private bool showUserModal;
private bool isEditingUser;
private bool isSaving;
private string? modalError;
private Guid editUserId;
private string editUsername = "";
private string editDisplayName = "";
private UserRole editRole = UserRole.User;
private bool editIsActive = true;
private string newPassword = "";
private string? usernameError;
private string? passwordError;
// Password Modal
private bool showPasswordModal;
private AppUser? passwordUser;
private string confirmPassword = "";
// Delete Modal
private bool showDeleteModal;
private AppUser? deleteUser;
// 2FA Setup Modal
private bool show2FaSetupModal;
private AppUser? setup2FaUser;
private string? setup2FaSecret;
private string? setup2FaQrDataUrl;
private string? setup2FaCode;
private bool setup2FaEnforceAfterConfirm;
protected override async Task OnInitializedAsync()
{
if (UserSession.IsAdmin)
{
await LoadUsers();
var tfSettings = await SiteSettingsService.GetTwoFactorSettingsAsync();
twoFactorGlobalEnabled = tfSettings.IsEnabled;
}
}
private DotNetObjectReference<Users>? _visibilityRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && UserSession.IsAdmin)
{
_visibilityRef = DotNetObjectReference.Create(this);
await VisibilityRefresh.RegisterAsync(JS, _visibilityRef, nameof(OnVisibilityReturn), 120);
}
}
[JSInvokable]
public async Task OnVisibilityReturn(int hiddenSeconds)
{
if (!UserSession.IsAdmin) return;
await LoadUsers();
await InvokeAsync(StateHasChanged);
}
public async ValueTask DisposeAsync()
{
await VisibilityRefresh.UnregisterAsync(JS);
_visibilityRef?.Dispose();
}
private async Task LoadUsers()
{
isLoading = true;
errorMessage = null;
StateHasChanged();
try
{
users = await AuthService.GetAllUsersAsync();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Laden der Benutzer: {ex.Message}";
}
finally
{
isLoading = false;
}
}
#region User Modal
private void OpenCreateUserModal()
{
editUserId = Guid.Empty;
editUsername = "";
editDisplayName = "";
editRole = UserRole.User;
editIsActive = true;
newPassword = "";
usernameError = null;
passwordError = null;
isEditingUser = false;
modalError = null;
showUserModal = true;
}
private void OpenEditUserModal(AppUser user)
{
editUserId = user.Id;
editUsername = user.Username;
editDisplayName = user.DisplayName ?? "";
editRole = user.Role;
editIsActive = user.IsActive;
newPassword = "";
usernameError = null;
passwordError = null;
isEditingUser = true;
modalError = null;
showUserModal = true;
}
private void CloseUserModal()
{
if (!isSaving)
{
showUserModal = false;
ResetUserForm();
}
}
private void ResetUserForm()
{
editUserId = Guid.Empty;
editUsername = "";
editDisplayName = "";
editRole = UserRole.User;
editIsActive = true;
newPassword = "";
usernameError = null;
passwordError = null;
modalError = null;
}
private void ValidateUsernameField()
{
var validation = AuthService.ValidateUsername(editUsername);
usernameError = validation.IsValid ? null : validation.ErrorMessage;
}
private void ValidatePasswordField()
{
if (!isEditingUser)
{
var validation = AuthService.ValidatePassword(newPassword);
passwordError = validation.IsValid ? null : validation.ErrorMessage;
}
}
private async Task SaveUser()
{
modalError = null;
usernameError = null;
passwordError = null;
// Validate username
var usernameValidation = AuthService.ValidateUsername(editUsername);
if (!usernameValidation.IsValid)
{
usernameError = usernameValidation.ErrorMessage;
return;
}
// Validate password for new users
if (!isEditingUser)
{
var passwordValidation = AuthService.ValidatePassword(newPassword);
if (!passwordValidation.IsValid)
{
passwordError = passwordValidation.ErrorMessage;
return;
}
}
isSaving = true;
StateHasChanged();
try
{
if (isEditingUser)
{
var updated = await AuthService.UpdateUserAsync(editUserId, editUsername, editDisplayName, editRole, editIsActive);
var index = users.FindIndex(u => u.Id == editUserId);
if (index >= 0)
{
users[index] = updated;
}
// Update current session if editing self
if (editUserId == UserSession.CurrentUser?.Id)
{
UserSession.UpdateUser(updated);
}
successMessage = $"Benutzer '{updated.Username}' wurde aktualisiert.";
}
else
{
var created = await AuthService.CreateUserAsync(editUsername, newPassword, editDisplayName, editRole);
users.Add(created);
users = users.OrderBy(u => u.Username).ToList();
successMessage = $"Benutzer '{created.Username}' wurde erstellt.";
}
showUserModal = false;
ResetUserForm();
}
catch (Exception ex)
{
modalError = ex.Message;
}
finally
{
isSaving = false;
}
}
#endregion
#region Password Modal
private void OpenPasswordModal(AppUser user)
{
passwordUser = user;
newPassword = "";
confirmPassword = "";
modalError = null;
showPasswordModal = true;
}
private void ClosePasswordModal()
{
if (!isSaving)
{
showPasswordModal = false;
passwordUser = null;
newPassword = "";
confirmPassword = "";
modalError = null;
}
}
private async Task ChangePassword()
{
if (passwordUser == null) return;
var validation = AuthService.ValidatePassword(newPassword);
if (!validation.IsValid)
{
modalError = validation.ErrorMessage;
return;
}
if (newPassword != confirmPassword)
{
modalError = "Passwörter stimmen nicht überein.";
return;
}
isSaving = true;
modalError = null;
StateHasChanged();
try
{
await AuthService.ChangePasswordAsync(passwordUser.Id, newPassword);
successMessage = $"Passwort für '{passwordUser.Username}' wurde geändert.";
showPasswordModal = false;
passwordUser = null;
newPassword = "";
confirmPassword = "";
}
catch (Exception ex)
{
modalError = ex.Message;
}
finally
{
isSaving = false;
}
}
#endregion
#region Delete Modal
private void OpenDeleteModal(AppUser user)
{
deleteUser = user;
modalError = null;
showDeleteModal = true;
}
private void CloseDeleteModal()
{
if (!isSaving)
{
showDeleteModal = false;
deleteUser = null;
modalError = null;
}
}
private async Task DeleteUser()
{
if (deleteUser == null) return;
isSaving = true;
modalError = null;
StateHasChanged();
try
{
var username = deleteUser.Username;
await AuthService.DeleteUserAsync(deleteUser.Id);
users.RemoveAll(u => u.Id == deleteUser.Id);
successMessage = $"Benutzer '{username}' wurde gelöscht.";
showDeleteModal = false;
deleteUser = null;
}
catch (Exception ex)
{
modalError = ex.Message;
}
finally
{
isSaving = false;
}
}
#endregion
#region 2FA Actions
private async Task Reset2Fa(AppUser user)
{
try
{
await AuthService.Reset2FaAsync(user.Id);
user.TwoFactorEnabled = false;
user.TwoFactorSecret = null;
user.TwoFactorEnforced = false;
successMessage = $"2FA für '{user.Username}' wurde zurückgesetzt.";
}
catch (Exception ex)
{
errorMessage = $"Fehler: {ex.Message}";
}
}
private async Task Enforce2Fa(AppUser user, bool enforce)
{
try
{
await AuthService.SetEnforce2FaAsync(user.Id, enforce);
user.TwoFactorEnforced = enforce;
if (enforce && !user.TwoFactorEnabled)
{
// User doesn't have 2FA set up yet — open setup modal with QR code
await Open2FaSetupModal(user, enforceAfterConfirm: true);
}
else
{
successMessage = enforce
? $"2FA wird für '{user.Username}' erzwungen."
: $"2FA-Zwang für '{user.Username}' aufgehoben.";
}
}
catch (Exception ex)
{
errorMessage = $"Fehler: {ex.Message}";
}
}
private async Task Open2FaSetupModal(AppUser user, bool enforceAfterConfirm = false)
{
try
{
var result = await AuthService.Enable2FaAsync(user.Id);
setup2FaUser = user;
setup2FaSecret = result.Secret;
setup2FaQrDataUrl = TwoFactorService.GenerateQrCodeDataUrl(result.OtpAuthUri);
setup2FaCode = "";
setup2FaEnforceAfterConfirm = enforceAfterConfirm;
modalError = null;
show2FaSetupModal = true;
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Einrichten von 2FA: {ex.Message}";
}
}
private async Task Confirm2FaSetup()
{
if (setup2FaUser == null || string.IsNullOrWhiteSpace(setup2FaCode)) return;
isSaving = true;
modalError = null;
StateHasChanged();
try
{
var confirmed = await AuthService.Confirm2FaAsync(setup2FaUser.Id, setup2FaCode);
if (confirmed)
{
setup2FaUser.TwoFactorEnabled = true;
setup2FaUser.TwoFactorSecret = setup2FaSecret;
if (setup2FaEnforceAfterConfirm)
{
setup2FaUser.TwoFactorEnforced = true;
}
successMessage = $"2FA für '{setup2FaUser.Username}' wurde erfolgreich eingerichtet.";
show2FaSetupModal = false;
Close2FaSetupModal();
}
else
{
modalError = "Ungültiger Code. Bitte versuchen Sie es erneut.";
}
}
catch (Exception ex)
{
modalError = ex.Message;
}
finally
{
isSaving = false;
}
}
private void Close2FaSetupModal()
{
if (!isSaving)
{
show2FaSetupModal = false;
setup2FaUser = null;
setup2FaSecret = null;
setup2FaQrDataUrl = null;
setup2FaCode = null;
setup2FaEnforceAfterConfirm = false;
modalError = null;
}
}
#endregion
#region Helper Methods
private static string GetRoleName(UserRole role) => role switch
{
UserRole.Admin => "Administrator",
UserRole.Worker => "Mitarbeiter",
UserRole.User => "Benutzer",
_ => ""
};
private static string GetRoleClass(UserRole role) => role switch
{
UserRole.Admin => "role-admin",
UserRole.Worker => "role-worker",
UserRole.User => "role-user",
_ => ""
};
private static string FormatLastLogin(DateTime lastLogin)
{
var local = lastLogin.ToLocalTime();
var diff = DateTime.Now - local;
if (diff.TotalMinutes < 1)
return "Gerade eben";
if (diff.TotalMinutes < 60)
return $"Vor {(int)diff.TotalMinutes} Min.";
if (diff.TotalHours < 24)
return $"Vor {(int)diff.TotalHours} Std.";
if (diff.TotalDays < 7)
return $"Vor {(int)diff.TotalDays} Tagen";
return local.ToString("dd.MM.yy HH:mm");
}
#endregion
}