multi-tenant implementierung

This commit is contained in:
MarcWieland
2026-06-24 23:48:04 +02:00
parent d646cfd28d
commit ac06059e2a
41 changed files with 2530 additions and 238 deletions
@@ -1,10 +1,14 @@
@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>
@@ -42,6 +46,7 @@ else
<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>
@@ -74,6 +79,34 @@ else
</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)
{
@@ -92,6 +125,12 @@ else
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"
@@ -114,9 +153,11 @@ else
@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()
{
@@ -125,13 +166,31 @@ else
if (claim == null) return;
UserNotificationService.OnUsersChanged += RefreshUsers;
_users = await AuthService.GetAllUsersAsync();
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);
}
@@ -144,27 +203,49 @@ else
{
_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();
var error = await AuthService.RenameUserAsync(user.Id, trimmed);
if (error != null)
// 1. Rename if username changed
if (trimmed != user.Username)
{
Snackbar.Add(error, Severity.Error);
return;
var error = await AuthService.RenameUserAsync(user.Id, trimmed);
if (error != null)
{
Snackbar.Add(error, Severity.Error);
return;
}
user.Username = trimmed;
}
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 = "";
Snackbar.Add($"Benutzer umbenannt zu \"{trimmed}\".", Severity.Success);
_editTenantId = null;
Snackbar.Add("Benutzerdaten erfolgreich aktualisiert.", Severity.Success);
}
private async Task DeleteUser(User user)
@@ -173,4 +254,29 @@ else
_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);
}
}
}
}