Files

984 lines
39 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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
}