2026-05-25 06:38:16 +02:00
|
|
|
|
@page "/user-management"
|
2026-06-21 07:52:03 +02:00
|
|
|
|
@attribute [Authorize(Roles = "Administrator")]
|
2026-05-25 06:38:16 +02:00
|
|
|
|
@inject IAuthService AuthService
|
2026-06-21 07:52:03 +02:00
|
|
|
|
@inject IEmailService EmailService
|
2026-05-25 06:38:16 +02:00
|
|
|
|
@inject ILocalizationService Loc
|
|
|
|
|
|
@inject NavigationManager Navigation
|
|
|
|
|
|
|
2026-06-21 07:52:03 +02:00
|
|
|
|
@rendermode InteractiveServer
|
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
|
<PageTitle>@Loc["usermanagement.title"] - Author Buddy</PageTitle>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="usermanagement-page">
|
2026-06-21 07:52:03 +02:00
|
|
|
|
<div class="usermanagement-header">
|
|
|
|
|
|
<h3>@Loc["usermanagement.title"]</h3>
|
|
|
|
|
|
<button class="btn btn-primary" @onclick="ShowCreateDialog">+ Neuen Benutzer</button>
|
|
|
|
|
|
</div>
|
2026-05-25 06:38:16 +02:00
|
|
|
|
|
|
|
|
|
|
<table class="usermanagement-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
2026-06-21 07:52:03 +02:00
|
|
|
|
<th>Name</th>
|
|
|
|
|
|
<th>E-Mail</th>
|
|
|
|
|
|
<th>Rolle</th>
|
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
|
<th>Aktionen</th>
|
2026-05-25 06:38:16 +02:00
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
@foreach (var u in users)
|
|
|
|
|
|
{
|
|
|
|
|
|
<tr class="@(u.Id == currentUserId ? "usermanagement-current" : "") @(!u.IsActive ? "usermanagement-inactive" : "")">
|
|
|
|
|
|
<td>@u.Name @(u.Id == currentUserId ? "(Du)" : "")</td>
|
2026-06-21 07:52:03 +02:00
|
|
|
|
<td>@(!string.IsNullOrEmpty(u.Email) ? u.Email : "—")</td>
|
|
|
|
|
|
<td>@string.Join(", ", u.Roles.Select(RoleName))</td>
|
2026-05-25 06:38:16 +02:00
|
|
|
|
<td>
|
|
|
|
|
|
@if (u.IsActive)
|
|
|
|
|
|
{
|
2026-06-21 07:52:03 +02:00
|
|
|
|
<span class="usermanagement-status-active">Aktiv</span>
|
2026-05-25 06:38:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-06-21 07:52:03 +02:00
|
|
|
|
<span class="usermanagement-status-inactive">Inaktiv</span>
|
2026-05-25 06:38:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="usermanagement-actions">
|
2026-06-21 07:52:03 +02:00
|
|
|
|
<button class="btn btn-sm" @onclick="() => BeginEdit(u)" title="Bearbeiten">✎</button>
|
|
|
|
|
|
@if (!string.IsNullOrEmpty(u.Email) && EmailService.IsConfigured)
|
|
|
|
|
|
{
|
|
|
|
|
|
<button class="btn btn-sm" @onclick="() => SendRecovery(u)" title="Passwort zurücksetzen per E-Mail">📧</button>
|
|
|
|
|
|
}
|
2026-05-25 06:38:16 +02:00
|
|
|
|
@if (u.Id != currentUserId)
|
|
|
|
|
|
{
|
|
|
|
|
|
@if (u.IsActive)
|
|
|
|
|
|
{
|
2026-06-21 07:52:03 +02:00
|
|
|
|
<button class="btn btn-sm" @onclick="() => DeactivateUser(u.Id)" title="Deaktivieren">🔴</button>
|
2026-05-25 06:38:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-06-21 07:52:03 +02:00
|
|
|
|
<button class="btn btn-sm btn-primary" @onclick="() => ActivateUser(u.Id)" title="Aktivieren">🟢</button>
|
2026-05-25 06:38:16 +02:00
|
|
|
|
}
|
2026-06-21 07:52:03 +02:00
|
|
|
|
<button class="btn btn-sm btn-danger" @onclick="() => BeginDelete(u)" title="Löschen">🗑</button>
|
2026-05-25 06:38:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
|
|
@if (!string.IsNullOrEmpty(statusMessage))
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="usermanagement-status">@statusMessage</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-21 07:52:03 +02:00
|
|
|
|
@* Create User Dialog *@
|
|
|
|
|
|
@if (showCreateDialog)
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="modal-overlay">
|
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
|
<div class="modal-title">Neuen Benutzer anlegen</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Benutzername</label>
|
|
|
|
|
|
<input @bind="_newUsername" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>E-Mail</label>
|
|
|
|
|
|
<input @bind="_newEmail" type="email" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Rollen</label>
|
|
|
|
|
|
<div class="um-role-checkboxes">
|
|
|
|
|
|
@foreach (var (role, label) in UmRoleOptions)
|
|
|
|
|
|
{
|
|
|
|
|
|
<label><input type="checkbox" checked="@_newRoles.Contains(role)" @onchange="() => ToggleNewRole(role)" /> @label</label>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Passwort</label>
|
|
|
|
|
|
<div style="display:flex;gap:8px;">
|
|
|
|
|
|
<input @bind="_newPassword" type="password" style="flex:1;" />
|
|
|
|
|
|
<button class="btn btn-sm" @onclick="GeneratePassword" type="button">Generieren</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
@if (!string.IsNullOrEmpty(_generatedPassword))
|
|
|
|
|
|
{
|
|
|
|
|
|
<small style="color:#c00;font-family:monospace;">@_generatedPassword</small>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
|
<button @onclick="CancelCreate">Abbrechen</button>
|
2026-07-04 10:32:35 +02:00
|
|
|
|
<button @onclick="CreateUser" class="primary" disabled="@_createUserDisabled">Anlegen</button>
|
2026-06-21 07:52:03 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@* Edit User Dialog *@
|
|
|
|
|
|
@if (showEditDialog && _editUser != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="modal-overlay">
|
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
|
<div class="modal-title">Benutzer bearbeiten – @_editUser.Name</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>E-Mail</label>
|
|
|
|
|
|
<input @bind="_editEmail" type="email" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>Rollen</label>
|
|
|
|
|
|
<div class="um-role-checkboxes">
|
|
|
|
|
|
@foreach (var (role, label) in UmRoleOptions)
|
|
|
|
|
|
{
|
|
|
|
|
|
<label><input type="checkbox" checked="@_editRoles.Contains(role)" @onchange="() => ToggleEditRole(role)" /> @label</label>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
|
<button @onclick="CancelEdit">Abbrechen</button>
|
|
|
|
|
|
<button @onclick="SaveEdit" class="primary">Speichern</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@* Delete Confirmation Dialog *@
|
|
|
|
|
|
@if (showDeleteDialog && _deleteUser != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="modal-overlay">
|
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
|
<div class="modal-title">Benutzer löschen</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<p>Soll der Benutzer <strong>@_deleteUser.Name</strong> wirklich gelöscht werden?</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
|
<button @onclick="CancelDelete">Abbrechen</button>
|
|
|
|
|
|
<button @onclick="ConfirmDelete" class="btn-danger">Löschen</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
|
@code {
|
|
|
|
|
|
private List<UserEntity> users = new();
|
|
|
|
|
|
private int currentUserId;
|
|
|
|
|
|
private string statusMessage = string.Empty;
|
|
|
|
|
|
|
2026-06-21 07:52:03 +02:00
|
|
|
|
private bool showCreateDialog;
|
|
|
|
|
|
private string _newUsername = string.Empty;
|
|
|
|
|
|
private string _newEmail = string.Empty;
|
|
|
|
|
|
private List<string> _newRoles = new() { "Author" };
|
|
|
|
|
|
private string _newPassword = string.Empty;
|
2026-07-04 10:32:35 +02:00
|
|
|
|
private bool _createUserDisabled => string.IsNullOrWhiteSpace(_newUsername) || string.IsNullOrWhiteSpace(_newPassword);
|
2026-06-21 07:52:03 +02:00
|
|
|
|
private string? _generatedPassword;
|
|
|
|
|
|
|
|
|
|
|
|
private bool showEditDialog;
|
|
|
|
|
|
private UserEntity? _editUser;
|
|
|
|
|
|
private string _editEmail = string.Empty;
|
|
|
|
|
|
private List<string> _editRoles = new();
|
|
|
|
|
|
|
|
|
|
|
|
private bool showDeleteDialog;
|
|
|
|
|
|
private UserEntity? _deleteUser;
|
|
|
|
|
|
|
|
|
|
|
|
private static (string role, string label)[] UmRoleOptions => new[]
|
|
|
|
|
|
{
|
|
|
|
|
|
("Author", "Autor"),
|
|
|
|
|
|
("Blogger", "Blogger"),
|
|
|
|
|
|
("Chat", "Chat"),
|
|
|
|
|
|
("Administrator", "Administrator")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
private string RoleName(UserRole role) => role switch
|
|
|
|
|
|
{
|
|
|
|
|
|
UserRole.Administrator => "Administrator",
|
|
|
|
|
|
UserRole.Blogger => "Blogger",
|
|
|
|
|
|
UserRole.Chat => "Chat",
|
|
|
|
|
|
_ => "Autor"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
await LoadUsers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task LoadUsers()
|
|
|
|
|
|
{
|
|
|
|
|
|
users = await AuthService.GetAllUsersAsync();
|
|
|
|
|
|
currentUserId = AuthService.CurrentUser?.Id ?? 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task ActivateUser(int userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ok = await AuthService.SetUserActiveAsync(userId, true);
|
2026-06-21 07:52:03 +02:00
|
|
|
|
statusMessage = ok ? "Benutzer aktiviert." : "Fehler";
|
2026-05-25 06:38:16 +02:00
|
|
|
|
await LoadUsers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task DeactivateUser(int userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ok = await AuthService.SetUserActiveAsync(userId, false);
|
2026-06-21 07:52:03 +02:00
|
|
|
|
statusMessage = ok ? "Benutzer deaktiviert." : "Fehler";
|
2026-05-25 06:38:16 +02:00
|
|
|
|
await LoadUsers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 07:52:03 +02:00
|
|
|
|
private void ShowCreateDialog()
|
|
|
|
|
|
{
|
|
|
|
|
|
_newUsername = string.Empty;
|
|
|
|
|
|
_newEmail = string.Empty;
|
|
|
|
|
|
_newRoles = new List<string> { "Author" };
|
|
|
|
|
|
_newPassword = string.Empty;
|
|
|
|
|
|
_generatedPassword = null;
|
|
|
|
|
|
showCreateDialog = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void CancelCreate()
|
|
|
|
|
|
{
|
|
|
|
|
|
showCreateDialog = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ToggleNewRole(string role)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_newRoles.Contains(role))
|
|
|
|
|
|
_newRoles.Remove(role);
|
|
|
|
|
|
else
|
|
|
|
|
|
_newRoles.Add(role);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ToggleEditRole(string role)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_editRoles.Contains(role))
|
|
|
|
|
|
_editRoles.Remove(role);
|
|
|
|
|
|
else
|
|
|
|
|
|
_editRoles.Add(role);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void GeneratePassword()
|
2026-05-25 06:38:16 +02:00
|
|
|
|
{
|
2026-06-21 07:52:03 +02:00
|
|
|
|
using var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
|
|
|
|
|
|
var bytes = new byte[10];
|
|
|
|
|
|
rng.GetBytes(bytes);
|
|
|
|
|
|
_generatedPassword = Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', 'X').Replace('/', 'Y');
|
|
|
|
|
|
_newPassword = _generatedPassword;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task CreateUser()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(_newUsername) || string.IsNullOrWhiteSpace(_newPassword))
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
if (_newRoles.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
statusMessage = "Mindestens eine Rolle erforderlich.";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var roles = _newRoles.Select(r => r switch
|
|
|
|
|
|
{
|
|
|
|
|
|
"Administrator" => UserRole.Administrator,
|
|
|
|
|
|
"Blogger" => UserRole.Blogger,
|
|
|
|
|
|
"Chat" => UserRole.Chat,
|
|
|
|
|
|
_ => UserRole.Author
|
|
|
|
|
|
}).Distinct().ToList();
|
|
|
|
|
|
|
|
|
|
|
|
var ok = await AuthService.CreateUserAsync(_newUsername.Trim(), _newPassword, _newEmail.Trim(), roles);
|
|
|
|
|
|
statusMessage = ok
|
|
|
|
|
|
? $"Benutzer '{_newUsername}' angelegt."
|
|
|
|
|
|
: "Benutzername bereits vergeben oder maximal ein Admin erlaubt.";
|
|
|
|
|
|
showCreateDialog = false;
|
2026-05-25 06:38:16 +02:00
|
|
|
|
await LoadUsers();
|
|
|
|
|
|
}
|
2026-06-21 07:52:03 +02:00
|
|
|
|
|
|
|
|
|
|
private void BeginEdit(UserEntity user)
|
|
|
|
|
|
{
|
|
|
|
|
|
_editUser = user;
|
|
|
|
|
|
_editEmail = user.Email;
|
|
|
|
|
|
_editRoles = user.Roles.Select(r => r.ToString()).ToList();
|
|
|
|
|
|
showEditDialog = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void CancelEdit()
|
|
|
|
|
|
{
|
|
|
|
|
|
showEditDialog = false;
|
|
|
|
|
|
_editUser = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task SaveEdit()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_editUser == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (_editRoles.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
statusMessage = "Mindestens eine Rolle erforderlich.";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var roles = _editRoles.Select(r => r switch
|
|
|
|
|
|
{
|
|
|
|
|
|
"Administrator" => UserRole.Administrator,
|
|
|
|
|
|
"Blogger" => UserRole.Blogger,
|
|
|
|
|
|
"Chat" => UserRole.Chat,
|
|
|
|
|
|
_ => UserRole.Author
|
|
|
|
|
|
}).Distinct().ToList();
|
|
|
|
|
|
|
|
|
|
|
|
string? email = _editEmail?.Trim();
|
|
|
|
|
|
if (string.IsNullOrEmpty(email)) email = null;
|
|
|
|
|
|
|
|
|
|
|
|
var ok = await AuthService.UpdateUserAsync(_editUser.Id, email, roles);
|
|
|
|
|
|
statusMessage = ok ? "Benutzer aktualisiert." : "Fehler (max. ein Admin erlaubt).";
|
|
|
|
|
|
showEditDialog = false;
|
|
|
|
|
|
_editUser = null;
|
|
|
|
|
|
await LoadUsers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void BeginDelete(UserEntity user)
|
|
|
|
|
|
{
|
|
|
|
|
|
_deleteUser = user;
|
|
|
|
|
|
showDeleteDialog = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void CancelDelete()
|
|
|
|
|
|
{
|
|
|
|
|
|
showDeleteDialog = false;
|
|
|
|
|
|
_deleteUser = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task ConfirmDelete()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_deleteUser == null) return;
|
|
|
|
|
|
var ok = await AuthService.DeleteUserAsync(_deleteUser.Id);
|
|
|
|
|
|
statusMessage = ok ? "Benutzer gelöscht." : "Fehler";
|
|
|
|
|
|
showDeleteDialog = false;
|
|
|
|
|
|
_deleteUser = null;
|
|
|
|
|
|
await LoadUsers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task SendRecovery(UserEntity user)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(user.Email)) return;
|
|
|
|
|
|
|
|
|
|
|
|
var newPassword = await AuthService.ResetPasswordAsync(user.Id);
|
|
|
|
|
|
if (string.IsNullOrEmpty(newPassword))
|
|
|
|
|
|
{
|
|
|
|
|
|
statusMessage = "Fehler beim Zurücksetzen des Passworts.";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var sent = await EmailService.SendPasswordAsync(user.Email, user.Name, newPassword);
|
|
|
|
|
|
statusMessage = sent
|
|
|
|
|
|
? $"Neues Passwort an {user.Email} gesendet."
|
|
|
|
|
|
: "E-Mail konnte nicht gesendet werden (SMTP-Konfiguration prüfen).";
|
|
|
|
|
|
}
|
2026-05-25 06:38:16 +02:00
|
|
|
|
}
|