984 lines
39 KiB
Plaintext
984 lines
39 KiB
Plaintext
@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
|
||
}
|