283 lines
12 KiB
Plaintext
283 lines
12 KiB
Plaintext
@page "/admin/users"
|
||
@rendermode InteractiveWebAssembly
|
||
@attribute [Authorize(Policy = "AdminOnly")]
|
||
@using System.Net.Http.Json
|
||
@using System.Security.Claims
|
||
@inject IAuthService AuthService
|
||
@inject ISnackbar Snackbar
|
||
@inject AuthenticationStateProvider AuthStateProvider
|
||
@inject IUserNotificationService UserNotificationService
|
||
@inject HttpClient Http
|
||
@inject IDialogService DialogService
|
||
@implements IDisposable
|
||
|
||
<PageTitle>Benutzerverwaltung – Timetracker</PageTitle>
|
||
|
||
@if (_loading)
|
||
{
|
||
<MudStack AlignItems="AlignItems.Center" Class="mt-16" Spacing="3">
|
||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
|
||
<MudText Color="Color.Secondary">Lade Benutzer…</MudText>
|
||
</MudStack>
|
||
}
|
||
else
|
||
{
|
||
<MudStack Spacing="4">
|
||
|
||
@* ── Header ── *@
|
||
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||
Style="background: #991B1B; color:white;">
|
||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
|
||
<MudIcon Icon="@Icons.Material.Filled.AdminPanelSettings" Style="color:white; font-size:2rem" />
|
||
<MudStack Spacing="0">
|
||
<MudText Typo="Typo.h5" Style="color:white; font-weight:700">Benutzerverwaltung</MudText>
|
||
<MudText Typo="Typo.caption" Style="color:rgba(255,255,255,0.72)">
|
||
@_users.Count Benutzer registriert
|
||
</MudText>
|
||
</MudStack>
|
||
</MudStack>
|
||
</MudPaper>
|
||
|
||
@* ── Tabelle ── *@
|
||
<MudCard Elevation="3" Class="rounded-xl">
|
||
<MudCardContent Class="pa-0">
|
||
<MudTable Items="_users" Hover="true" Striped="true" Dense="false"
|
||
SortLabel="Sortieren">
|
||
<HeaderContent>
|
||
<MudTh><MudTableSortLabel SortBy="new Func<User, object>(u => u.Id)">ID</MudTableSortLabel></MudTh>
|
||
<MudTh><MudTableSortLabel SortBy="new Func<User, object>(u => u.Username)" InitialDirection="SortDirection.Ascending">Benutzername</MudTableSortLabel></MudTh>
|
||
<MudTh>Firma / Mandant</MudTh>
|
||
<MudTh Style="text-align:right">Aktionen</MudTh>
|
||
</HeaderContent>
|
||
<RowTemplate>
|
||
<MudTd>
|
||
<MudText Typo="Typo.body2" Color="Color.Secondary">@context.Id</MudText>
|
||
</MudTd>
|
||
<MudTd>
|
||
@if (_editUserId == context.Id)
|
||
{
|
||
<MudTextField @bind-Value="_editUsername"
|
||
Variant="Variant.Outlined"
|
||
Margin="Margin.Dense"
|
||
Immediate="true"
|
||
Style="max-width:220px"
|
||
OnKeyDown="@(async e => { if (e.Key == "Enter") await SaveRename(context); })" />
|
||
}
|
||
else
|
||
{
|
||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||
<MudIcon Icon="@Icons.Material.Filled.AccountCircle"
|
||
Color="@(context.Username == "marc" ? Color.Error : Color.Default)"
|
||
Size="Size.Small" />
|
||
<MudText Style="@(context.Username == "marc" ? "font-weight:700" : "")">
|
||
@context.Username
|
||
</MudText>
|
||
@if (context.Username == "marc")
|
||
{
|
||
<MudChip T="string" Size="Size.Small" Color="Color.Error" Variant="Variant.Outlined">Admin</MudChip>
|
||
}
|
||
</MudStack>
|
||
}
|
||
</MudTd>
|
||
<MudTd>
|
||
@if (_editUserId == context.Id)
|
||
{
|
||
<MudSelect @bind-Value="_editTenantId" Margin="Margin.Dense" T="int?" Label="Firma zuweisen" Variant="Variant.Outlined" Style="max-width:250px">
|
||
<MudSelectItem T="int?" Value="@((int?)null)">Hauptdomain (System/Global)</MudSelectItem>
|
||
@foreach (var tenant in _tenants)
|
||
{
|
||
<MudSelectItem T="int?" Value="@((int?)tenant.Id)">@tenant.Name (@tenant.Subdomain)</MudSelectItem>
|
||
}
|
||
</MudSelect>
|
||
}
|
||
else
|
||
{
|
||
@if (context.TenantId.HasValue)
|
||
{
|
||
var t = _tenants.FirstOrDefault(x => x.Id == context.TenantId.Value);
|
||
<MudChip T="string" Color="Color.Primary" Variant="Variant.Outlined" Size="Size.Small">
|
||
@(t?.Name ?? $"Firma #{context.TenantId}")
|
||
</MudChip>
|
||
}
|
||
else
|
||
{
|
||
<MudChip T="string" Color="Color.Default" Variant="Variant.Text" Size="Size.Small">
|
||
Hauptdomain (SuperUser)
|
||
</MudChip>
|
||
}
|
||
}
|
||
</MudTd>
|
||
<MudTd Style="text-align:right">
|
||
@if (_editUserId == context.Id)
|
||
{
|
||
<MudIconButton Icon="@Icons.Material.Filled.Check"
|
||
Color="Color.Success"
|
||
Size="Size.Small"
|
||
OnClick="@(() => SaveRename(context))" />
|
||
<MudIconButton Icon="@Icons.Material.Filled.Close"
|
||
Color="Color.Default"
|
||
Size="Size.Small"
|
||
OnClick="CancelEdit" />
|
||
}
|
||
else
|
||
{
|
||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||
Color="Color.Primary"
|
||
Size="Size.Small"
|
||
OnClick="@(() => StartEdit(context))" />
|
||
<MudTooltip Text="Passwort zurücksetzen">
|
||
<MudIconButton Icon="@Icons.Material.Filled.LockReset"
|
||
Color="Color.Warning"
|
||
Size="Size.Small"
|
||
OnClick="@(() => OpenResetPasswordDialog(context))" />
|
||
</MudTooltip>
|
||
@if (context.Username != "marc")
|
||
{
|
||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
||
Color="Color.Error"
|
||
Size="Size.Small"
|
||
OnClick="@(() => DeleteUser(context))" />
|
||
}
|
||
}
|
||
</MudTd>
|
||
</RowTemplate>
|
||
<NoRecordsContent>
|
||
<MudText Color="Color.Secondary" Class="pa-4">Keine Benutzer gefunden.</MudText>
|
||
</NoRecordsContent>
|
||
</MudTable>
|
||
</MudCardContent>
|
||
</MudCard>
|
||
|
||
</MudStack>
|
||
}
|
||
|
||
@code {
|
||
private List<User> _users = [];
|
||
private List<TenantStats> _tenants = [];
|
||
private bool _loading = true;
|
||
private int? _editUserId;
|
||
private string _editUsername = "";
|
||
private int? _editTenantId;
|
||
|
||
protected override async Task OnInitializedAsync()
|
||
{
|
||
var claim = (await AuthStateProvider.GetAuthenticationStateAsync())
|
||
.User.FindFirst(ClaimTypes.NameIdentifier);
|
||
if (claim == null) return;
|
||
|
||
UserNotificationService.OnUsersChanged += RefreshUsers;
|
||
|
||
try
|
||
{
|
||
_users = await AuthService.GetAllUsersAsync();
|
||
_tenants = await Http.GetFromJsonAsync<List<TenantStats>>("api/superadmin/tenants") ?? [];
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"Fehler beim Laden der Daten: {ex.Message}", Severity.Error);
|
||
}
|
||
|
||
_loading = false;
|
||
}
|
||
|
||
private async Task RefreshUsers()
|
||
{
|
||
_users = await AuthService.GetAllUsersAsync();
|
||
try
|
||
{
|
||
_tenants = await Http.GetFromJsonAsync<List<TenantStats>>("api/superadmin/tenants") ?? [];
|
||
}
|
||
catch
|
||
{
|
||
// Ignore background refresh errors
|
||
}
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
UserNotificationService.OnUsersChanged -= RefreshUsers;
|
||
}
|
||
|
||
private void StartEdit(User user)
|
||
{
|
||
_editUserId = user.Id;
|
||
_editUsername = user.Username;
|
||
_editTenantId = user.TenantId;
|
||
}
|
||
|
||
private void CancelEdit()
|
||
{
|
||
_editUserId = null;
|
||
_editUsername = "";
|
||
_editTenantId = null;
|
||
}
|
||
|
||
private async Task SaveRename(User user)
|
||
{
|
||
var trimmed = _editUsername.Trim();
|
||
|
||
// 1. Rename if username changed
|
||
if (trimmed != user.Username)
|
||
{
|
||
var error = await AuthService.RenameUserAsync(user.Id, trimmed);
|
||
if (error != null)
|
||
{
|
||
Snackbar.Add(error, Severity.Error);
|
||
return;
|
||
}
|
||
user.Username = trimmed;
|
||
}
|
||
|
||
// 2. Move to tenant if tenant ID changed
|
||
if (_editTenantId != user.TenantId)
|
||
{
|
||
var error = await AuthService.AssignTenantAsync(user.Id, _editTenantId);
|
||
if (error != null)
|
||
{
|
||
Snackbar.Add(error, Severity.Error);
|
||
return;
|
||
}
|
||
user.TenantId = _editTenantId;
|
||
}
|
||
|
||
_editUserId = null;
|
||
_editUsername = "";
|
||
_editTenantId = null;
|
||
|
||
Snackbar.Add("Benutzerdaten erfolgreich aktualisiert.", Severity.Success);
|
||
}
|
||
|
||
private async Task DeleteUser(User user)
|
||
{
|
||
await AuthService.DeleteUserAsync(user.Id);
|
||
_users.Remove(user);
|
||
Snackbar.Add($"Benutzer \"{user.Username}\" gelöscht.", Severity.Info);
|
||
}
|
||
|
||
private async Task OpenResetPasswordDialog(User user)
|
||
{
|
||
var parameters = new DialogParameters<ResetPasswordDialog>
|
||
{
|
||
{ x => x.Username, user.Username }
|
||
};
|
||
|
||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||
var dialog = await DialogService.ShowAsync<ResetPasswordDialog>("Passwort zurücksetzen", parameters, options);
|
||
var result = await dialog.Result;
|
||
|
||
if (result != null && !result.Canceled && result.Data is string newPassword)
|
||
{
|
||
var error = await AuthService.ResetPasswordAsync(user.Id, newPassword);
|
||
if (error != null)
|
||
{
|
||
Snackbar.Add(error, Severity.Error);
|
||
}
|
||
else
|
||
{
|
||
Snackbar.Add($"Passwort für {user.Username} wurde erfolgreich zurückgesetzt.", Severity.Success);
|
||
}
|
||
}
|
||
}
|
||
}
|