mirror of
https://github.com/claunia/marechai.git
synced 2025-12-16 19:14:25 +00:00
Add users management page.
This commit is contained in:
@@ -33,15 +33,16 @@
|
|||||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0"/>
|
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0"/>
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.11"/>
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.11"/>
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.11"/>
|
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.11"/>
|
||||||
|
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
|
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2"/>
|
||||||
<!-- Add more community toolkit references here -->
|
<!-- Add more community toolkit references here -->
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
||||||
<PackageVersion Include="Uno.CommunityToolkit.WinUI.UI.Controls" Version="7.1.200" />
|
<PackageVersion Include="Uno.CommunityToolkit.WinUI.UI.Controls" Version="7.1.200"/>
|
||||||
<!-- Add more uno community toolkit references here -->
|
<!-- Add more uno community toolkit references here -->
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||||
<local:InvertBoolToVisibilityConverter x:Key="InvertBoolToVisibilityConverter" />
|
<local:InvertBoolToVisibilityConverter x:Key="InvertBoolToVisibilityConverter" />
|
||||||
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||||
|
<local:RolesListConverter x:Key="RolesListConverter" />
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ public partial class App : Application
|
|||||||
AuthService>();
|
AuthService>();
|
||||||
|
|
||||||
services.AddSingleton<ITokenService, TokenService>();
|
services.AddSingleton<ITokenService, TokenService>();
|
||||||
|
services.AddSingleton<IJwtService, JwtService>();
|
||||||
services.AddSingleton<FlagCache>();
|
services.AddSingleton<FlagCache>();
|
||||||
services.AddSingleton<CompanyLogoCache>();
|
services.AddSingleton<CompanyLogoCache>();
|
||||||
services.AddSingleton<MachinePhotoCache>();
|
services.AddSingleton<MachinePhotoCache>();
|
||||||
@@ -200,6 +201,7 @@ public partial class App : Application
|
|||||||
new ViewMap<SoundSynthListPage, SoundSynthsListViewModel>(),
|
new ViewMap<SoundSynthListPage, SoundSynthsListViewModel>(),
|
||||||
new ViewMap<SoundSynthDetailPage, SoundSynthDetailViewModel>(),
|
new ViewMap<SoundSynthDetailPage, SoundSynthDetailViewModel>(),
|
||||||
new ViewMap<SettingsPage, SettingsViewModel>(),
|
new ViewMap<SettingsPage, SettingsViewModel>(),
|
||||||
|
new ViewMap<UsersPage, UsersViewModel>(),
|
||||||
new DataViewMap<SecondPage, SecondViewModel, Entity>());
|
new DataViewMap<SecondPage, SecondViewModel, Entity>());
|
||||||
|
|
||||||
routes.Register(new RouteMap("",
|
routes.Register(new RouteMap("",
|
||||||
@@ -247,7 +249,8 @@ public partial class App : Application
|
|||||||
]),
|
]),
|
||||||
new RouteMap("gpus",
|
new RouteMap("gpus",
|
||||||
views.FindByViewModel<GpuListViewModel>(),
|
views.FindByViewModel<GpuListViewModel>(),
|
||||||
Nested:
|
Nested
|
||||||
|
:
|
||||||
[
|
[
|
||||||
new RouteMap("gpu-details",
|
new RouteMap("gpu-details",
|
||||||
views.FindByViewModel<
|
views.FindByViewModel<
|
||||||
@@ -275,6 +278,8 @@ public partial class App : Application
|
|||||||
]),
|
]),
|
||||||
new RouteMap("settings",
|
new RouteMap("settings",
|
||||||
views.FindByViewModel<SettingsViewModel>()),
|
views.FindByViewModel<SettingsViewModel>()),
|
||||||
|
new RouteMap("users",
|
||||||
|
views.FindByViewModel<UsersViewModel>()),
|
||||||
new RouteMap("Second",
|
new RouteMap("Second",
|
||||||
views.FindByViewModel<SecondViewModel>())
|
views.FindByViewModel<SecondViewModel>())
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -41,10 +41,11 @@
|
|||||||
</UnoFeatures>
|
</UnoFeatures>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Marechai.Data\Marechai.Data.csproj" />
|
<ProjectReference Include="..\Marechai.Data\Marechai.Data.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Humanizer" />
|
<PackageReference Include="Humanizer"/>
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Presentation\Views\Shell.xaml.cs">
|
<Compile Update="Presentation\Views\Shell.xaml.cs">
|
||||||
@@ -79,16 +80,20 @@
|
|||||||
<DependentUpon>Sidebar.xaml</DependentUpon>
|
<DependentUpon>Sidebar.xaml</DependentUpon>
|
||||||
<IsDefaultItem>true</IsDefaultItem>
|
<IsDefaultItem>true</IsDefaultItem>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Update="Presentation\Views\UsersPage.xaml.cs">
|
||||||
|
<DependentUpon>UsersPage.xaml</DependentUpon>
|
||||||
|
<IsDefaultItem>true</IsDefaultItem>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" />
|
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls"/>
|
||||||
<!-- Add more community toolkit references here -->
|
<!-- Add more community toolkit references here -->
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
||||||
<PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls" />
|
<PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls"/>
|
||||||
<!-- Add more uno community toolkit references here -->
|
<!-- Add more uno community toolkit references here -->
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -231,6 +231,19 @@
|
|||||||
Visibility="{Binding IsSidebarOpen}">
|
Visibility="{Binding IsSidebarOpen}">
|
||||||
<StackPanel Orientation="Vertical"
|
<StackPanel Orientation="Vertical"
|
||||||
Spacing="0">
|
Spacing="0">
|
||||||
|
<!-- Users (Uberadmin only) -->
|
||||||
|
<Button Content="User Management"
|
||||||
|
Command="{Binding NavigateToUsersCommand}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Left"
|
||||||
|
Padding="16,10,16,10"
|
||||||
|
FontSize="13"
|
||||||
|
Background="Transparent"
|
||||||
|
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||||
|
BorderThickness="0"
|
||||||
|
CornerRadius="0"
|
||||||
|
Visibility="{Binding IsUberadminUser, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||||
|
|
||||||
<!-- Login/Logout -->
|
<!-- Login/Logout -->
|
||||||
<Button Content="{Binding LoginLogoutButtonText}"
|
<Button Content="{Binding LoginLogoutButtonText}"
|
||||||
Command="{Binding LoginLogoutCommand}"
|
Command="{Binding LoginLogoutCommand}"
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Marechai.App.Services;
|
using Marechai.App.Services;
|
||||||
|
using Marechai.App.Services.Authentication;
|
||||||
using Uno.Extensions.Authentication;
|
using Uno.Extensions.Authentication;
|
||||||
using Uno.Extensions.Navigation;
|
using Uno.Extensions.Navigation;
|
||||||
using Uno.Extensions.Toolkit;
|
using Uno.Extensions.Toolkit;
|
||||||
@@ -13,11 +15,15 @@ namespace Marechai.App.Presentation.ViewModels;
|
|||||||
public partial class MainViewModel : ObservableObject
|
public partial class MainViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly IAuthenticationService _authService;
|
private readonly IAuthenticationService _authService;
|
||||||
|
private readonly IJwtService _jwtService;
|
||||||
private readonly IStringLocalizer _localizer;
|
private readonly IStringLocalizer _localizer;
|
||||||
private readonly INavigator _navigator;
|
private readonly INavigator _navigator;
|
||||||
|
private readonly ITokenService _tokenService;
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _isSidebarOpen = true;
|
private bool _isSidebarOpen = true;
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
private bool _isUberadminUser;
|
||||||
|
[ObservableProperty]
|
||||||
private Dictionary<string, string> _localizedStrings = new();
|
private Dictionary<string, string> _localizedStrings = new();
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _loginLogoutButtonText = "";
|
private string _loginLogoutButtonText = "";
|
||||||
@@ -31,11 +37,13 @@ public partial class MainViewModel : ObservableObject
|
|||||||
|
|
||||||
public MainViewModel(IStringLocalizer localizer, IOptions<AppConfig> appInfo, INavigator navigator,
|
public MainViewModel(IStringLocalizer localizer, IOptions<AppConfig> appInfo, INavigator navigator,
|
||||||
NewsViewModel newsViewModel, IColorThemeService colorThemeService, IThemeService themeService,
|
NewsViewModel newsViewModel, IColorThemeService colorThemeService, IThemeService themeService,
|
||||||
IAuthenticationService authService)
|
IAuthenticationService authService, IJwtService jwtService, ITokenService tokenService)
|
||||||
{
|
{
|
||||||
_navigator = navigator;
|
_navigator = navigator;
|
||||||
_localizer = localizer;
|
_localizer = localizer;
|
||||||
_authService = authService;
|
_authService = authService;
|
||||||
|
_jwtService = jwtService;
|
||||||
|
_tokenService = tokenService;
|
||||||
NewsViewModel = newsViewModel;
|
NewsViewModel = newsViewModel;
|
||||||
Title = "Marechai";
|
Title = "Marechai";
|
||||||
Title += $" - {localizer["ApplicationName"]}";
|
Title += $" - {localizer["ApplicationName"]}";
|
||||||
@@ -63,6 +71,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
NavigateToProcessorsCommand = new AsyncRelayCommand(() => NavigateTo("processors"));
|
NavigateToProcessorsCommand = new AsyncRelayCommand(() => NavigateTo("processors"));
|
||||||
NavigateToSoftwareCommand = new AsyncRelayCommand(() => NavigateTo("software"));
|
NavigateToSoftwareCommand = new AsyncRelayCommand(() => NavigateTo("software"));
|
||||||
NavigateToSoundSynthesizersCommand = new AsyncRelayCommand(() => NavigateTo("sound-synths"));
|
NavigateToSoundSynthesizersCommand = new AsyncRelayCommand(() => NavigateTo("sound-synths"));
|
||||||
|
NavigateToUsersCommand = new AsyncRelayCommand(() => NavigateTo("users"));
|
||||||
NavigateToSettingsCommand = new AsyncRelayCommand(() => NavigateTo("settings"));
|
NavigateToSettingsCommand = new AsyncRelayCommand(() => NavigateTo("settings"));
|
||||||
LoginLogoutCommand = new RelayCommand(HandleLoginLogout);
|
LoginLogoutCommand = new RelayCommand(HandleLoginLogout);
|
||||||
ToggleSidebarCommand = new RelayCommand(() => IsSidebarOpen = !IsSidebarOpen);
|
ToggleSidebarCommand = new RelayCommand(() => IsSidebarOpen = !IsSidebarOpen);
|
||||||
@@ -71,6 +80,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
_authService.LoggedOut += OnLoggedOut;
|
_authService.LoggedOut += OnLoggedOut;
|
||||||
|
|
||||||
UpdateLoginLogoutButtonText();
|
UpdateLoginLogoutButtonText();
|
||||||
|
UpdateUberadminStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? Title { get; }
|
public string? Title { get; }
|
||||||
@@ -90,6 +100,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
public ICommand NavigateToProcessorsCommand { get; }
|
public ICommand NavigateToProcessorsCommand { get; }
|
||||||
public ICommand NavigateToSoftwareCommand { get; }
|
public ICommand NavigateToSoftwareCommand { get; }
|
||||||
public ICommand NavigateToSoundSynthesizersCommand { get; }
|
public ICommand NavigateToSoundSynthesizersCommand { get; }
|
||||||
|
public ICommand NavigateToUsersCommand { get; }
|
||||||
public ICommand NavigateToSettingsCommand { get; }
|
public ICommand NavigateToSettingsCommand { get; }
|
||||||
public ICommand LoginLogoutCommand { get; }
|
public ICommand LoginLogoutCommand { get; }
|
||||||
public ICommand ToggleSidebarCommand { get; }
|
public ICommand ToggleSidebarCommand { get; }
|
||||||
@@ -171,16 +182,38 @@ public partial class MainViewModel : ObservableObject
|
|||||||
LoginLogoutButtonText = isAuthenticated ? LocalizedStrings["Logout"] : LocalizedStrings["Login"];
|
LoginLogoutButtonText = isAuthenticated ? LocalizedStrings["Logout"] : LocalizedStrings["Login"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateUberadminStatus()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string token = _tokenService.GetToken();
|
||||||
|
|
||||||
|
if(!string.IsNullOrWhiteSpace(token))
|
||||||
|
{
|
||||||
|
IEnumerable<string> roles = _jwtService.GetRoles(token);
|
||||||
|
IsUberadminUser = roles.Contains("Uberadmin", StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
IsUberadminUser = false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
IsUberadminUser = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnLoggedOut(object? sender, EventArgs e)
|
private void OnLoggedOut(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
// Update button text when user logs out
|
// Update button text when user logs out
|
||||||
UpdateLoginLogoutButtonText();
|
UpdateLoginLogoutButtonText();
|
||||||
|
UpdateUberadminStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshAuthenticationState()
|
public void RefreshAuthenticationState()
|
||||||
{
|
{
|
||||||
// Public method to refresh authentication state (called after login)
|
// Public method to refresh authentication state (called after login)
|
||||||
UpdateLoginLogoutButtonText();
|
UpdateLoginLogoutButtonText();
|
||||||
|
UpdateUberadminStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void HandleLoginLogout()
|
private async void HandleLoginLogout()
|
||||||
@@ -192,6 +225,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
// Logout
|
// Logout
|
||||||
await _authService.LogoutAsync(null, CancellationToken.None);
|
await _authService.LogoutAsync(null, CancellationToken.None);
|
||||||
UpdateLoginLogoutButtonText();
|
UpdateLoginLogoutButtonText();
|
||||||
|
UpdateUberadminStatus();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
440
Marechai.App/Presentation/ViewModels/UsersViewModel.cs
Normal file
440
Marechai.App/Presentation/ViewModels/UsersViewModel.cs
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Marechai.App.Services.Authentication;
|
||||||
|
|
||||||
|
namespace Marechai.App.Presentation.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for user management page (Uberadmin only)
|
||||||
|
/// </summary>
|
||||||
|
public partial class UsersViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly ApiClient _apiClient;
|
||||||
|
private readonly IJwtService _jwtService;
|
||||||
|
private readonly IStringLocalizer _localizer;
|
||||||
|
private readonly ILogger<UsersViewModel> _logger;
|
||||||
|
private readonly ITokenService _tokenService;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ObservableCollection<string> _availableRoles = [];
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _confirmPassword = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _dialogTitle = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _dialogType = string.Empty;
|
||||||
|
|
||||||
|
private string? _editingUserId;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _email = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _errorMessage = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _hasError;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isDataLoaded;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isLoading;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _password = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _phoneNumber = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string? _selectedRole;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private UserDto? _selectedUser;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _userName = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ObservableCollection<string> _userRoles = [];
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ObservableCollection<UserDto> _users = [];
|
||||||
|
|
||||||
|
public UsersViewModel(ApiClient apiClient, IJwtService jwtService, ITokenService tokenService,
|
||||||
|
ILogger<UsersViewModel> logger, IStringLocalizer localizer)
|
||||||
|
{
|
||||||
|
_apiClient = apiClient;
|
||||||
|
_jwtService = jwtService;
|
||||||
|
_tokenService = tokenService;
|
||||||
|
_logger = logger;
|
||||||
|
_localizer = localizer;
|
||||||
|
|
||||||
|
LoadUsersCommand = new AsyncRelayCommand(LoadUsersAsync);
|
||||||
|
DeleteUserCommand = new AsyncRelayCommand<UserDto>(DeleteUserAsync);
|
||||||
|
OpenAddUserDialogCommand = new AsyncRelayCommand(OpenAddUserDialogAsync);
|
||||||
|
OpenEditUserDialogCommand = new AsyncRelayCommand<UserDto>(OpenEditUserDialogAsync);
|
||||||
|
OpenChangePasswordDialogCommand = new AsyncRelayCommand<UserDto>(OpenChangePasswordDialogAsync);
|
||||||
|
OpenManageRolesDialogCommand = new AsyncRelayCommand<UserDto>(OpenManageRolesDialogAsync);
|
||||||
|
SaveUserCommand = new AsyncRelayCommand(SaveUserAsync);
|
||||||
|
SavePasswordCommand = new AsyncRelayCommand(SavePasswordAsync);
|
||||||
|
AddRoleCommand = new AsyncRelayCommand(AddRoleAsync);
|
||||||
|
RemoveRoleCommand = new AsyncRelayCommand<string>(RemoveRoleAsync);
|
||||||
|
CloseDialogCommand = new RelayCommand(CloseDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAsyncRelayCommand LoadUsersCommand { get; }
|
||||||
|
public IAsyncRelayCommand<UserDto> DeleteUserCommand { get; }
|
||||||
|
public IAsyncRelayCommand OpenAddUserDialogCommand { get; }
|
||||||
|
public IAsyncRelayCommand<UserDto> OpenEditUserDialogCommand { get; }
|
||||||
|
public IAsyncRelayCommand<UserDto> OpenChangePasswordDialogCommand { get; }
|
||||||
|
public IAsyncRelayCommand<UserDto> OpenManageRolesDialogCommand { get; }
|
||||||
|
public IAsyncRelayCommand SaveUserCommand { get; }
|
||||||
|
public IAsyncRelayCommand SavePasswordCommand { get; }
|
||||||
|
public IAsyncRelayCommand AddRoleCommand { get; }
|
||||||
|
public IAsyncRelayCommand<string> RemoveRoleCommand { get; }
|
||||||
|
public IRelayCommand CloseDialogCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the current user is Uberadmin
|
||||||
|
/// </summary>
|
||||||
|
public bool IsUberadmin
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string? token = _tokenService.GetToken();
|
||||||
|
IEnumerable<string>? roles = _jwtService.GetRoles(token);
|
||||||
|
|
||||||
|
return roles.Contains("Uberadmin", StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<string>? ShowDialogRequested;
|
||||||
|
|
||||||
|
private async Task LoadUsersAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsLoading = true;
|
||||||
|
HasError = false;
|
||||||
|
ErrorMessage = string.Empty;
|
||||||
|
Users.Clear();
|
||||||
|
|
||||||
|
List<UserDto>? usersResponse = await _apiClient.Users.GetAsync();
|
||||||
|
|
||||||
|
if(usersResponse != null)
|
||||||
|
{
|
||||||
|
foreach(UserDto user in usersResponse) Users.Add(user);
|
||||||
|
|
||||||
|
IsDataLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error loading users");
|
||||||
|
ErrorMessage = _localizer["Failed to load users. Please try again."];
|
||||||
|
HasError = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteUserAsync(UserDto? user)
|
||||||
|
{
|
||||||
|
if(user?.Id == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _apiClient.Users[user.Id].DeleteAsync();
|
||||||
|
await LoadUsersAsync();
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting user");
|
||||||
|
ErrorMessage = _localizer["Failed to delete user."];
|
||||||
|
HasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenAddUserDialogAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_editingUserId = null;
|
||||||
|
DialogTitle = _localizer["Add User"];
|
||||||
|
DialogType = "AddEdit";
|
||||||
|
Email = string.Empty;
|
||||||
|
UserName = string.Empty;
|
||||||
|
PhoneNumber = string.Empty;
|
||||||
|
Password = string.Empty;
|
||||||
|
ConfirmPassword = string.Empty;
|
||||||
|
ShowDialogRequested?.Invoke(this, "AddEdit");
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error opening add user dialog");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenEditUserDialogAsync(UserDto? user)
|
||||||
|
{
|
||||||
|
if(user?.Id == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_editingUserId = user.Id;
|
||||||
|
DialogTitle = _localizer["Edit User"];
|
||||||
|
DialogType = "AddEdit";
|
||||||
|
Email = user.Email ?? string.Empty;
|
||||||
|
UserName = user.UserName ?? string.Empty;
|
||||||
|
PhoneNumber = user.PhoneNumber ?? string.Empty;
|
||||||
|
Password = string.Empty;
|
||||||
|
ConfirmPassword = string.Empty;
|
||||||
|
ShowDialogRequested?.Invoke(this, "AddEdit");
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error opening edit user dialog");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenChangePasswordDialogAsync(UserDto? user)
|
||||||
|
{
|
||||||
|
if(user?.Id == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_editingUserId = user.Id;
|
||||||
|
DialogTitle = _localizer["Change Password"];
|
||||||
|
DialogType = "Password";
|
||||||
|
Password = string.Empty;
|
||||||
|
ConfirmPassword = string.Empty;
|
||||||
|
ShowDialogRequested?.Invoke(this, "Password");
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error opening change password dialog");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenManageRolesDialogAsync(UserDto? user)
|
||||||
|
{
|
||||||
|
if(user?.Id == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_editingUserId = user.Id;
|
||||||
|
DialogTitle = _localizer["Manage Roles"];
|
||||||
|
DialogType = "Roles";
|
||||||
|
|
||||||
|
// Load available roles
|
||||||
|
List<string>? rolesResponse = await _apiClient.Users.Roles.GetAsync();
|
||||||
|
AvailableRoles.Clear();
|
||||||
|
|
||||||
|
if(rolesResponse != null)
|
||||||
|
{
|
||||||
|
foreach(string role in rolesResponse)
|
||||||
|
if(!string.IsNullOrWhiteSpace(role))
|
||||||
|
AvailableRoles.Add(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation($"Loaded {AvailableRoles.Count} available roles");
|
||||||
|
|
||||||
|
// Load user's current roles
|
||||||
|
UserRoles.Clear();
|
||||||
|
|
||||||
|
if(user.Roles != null)
|
||||||
|
foreach(string role in user.Roles)
|
||||||
|
UserRoles.Add(role);
|
||||||
|
|
||||||
|
ShowDialogRequested?.Invoke(this, "Roles");
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error opening manage roles dialog");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveUserAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(Email) || string.IsNullOrWhiteSpace(UserName))
|
||||||
|
{
|
||||||
|
ErrorMessage = _localizer["Email and username are required."];
|
||||||
|
HasError = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_editingUserId == null)
|
||||||
|
{
|
||||||
|
// Create new user
|
||||||
|
if(string.IsNullOrWhiteSpace(Password))
|
||||||
|
{
|
||||||
|
ErrorMessage = _localizer["Password is required for new users."];
|
||||||
|
HasError = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Password != ConfirmPassword)
|
||||||
|
{
|
||||||
|
ErrorMessage = _localizer["Passwords do not match."];
|
||||||
|
HasError = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var createRequest = new CreateUserRequest
|
||||||
|
{
|
||||||
|
Email = Email,
|
||||||
|
UserName = UserName,
|
||||||
|
PhoneNumber = PhoneNumber,
|
||||||
|
Password = Password
|
||||||
|
};
|
||||||
|
|
||||||
|
await _apiClient.Users.PostAsync(createRequest);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Update existing user
|
||||||
|
var updateRequest = new UpdateUserRequest
|
||||||
|
{
|
||||||
|
Email = Email,
|
||||||
|
UserName = UserName,
|
||||||
|
PhoneNumber = PhoneNumber
|
||||||
|
};
|
||||||
|
|
||||||
|
await _apiClient.Users[_editingUserId].PutAsync(updateRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseDialog();
|
||||||
|
await LoadUsersAsync();
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error saving user");
|
||||||
|
ErrorMessage = _localizer["Failed to save user."];
|
||||||
|
HasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SavePasswordAsync()
|
||||||
|
{
|
||||||
|
if(_editingUserId == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(Password))
|
||||||
|
{
|
||||||
|
ErrorMessage = _localizer["Password is required."];
|
||||||
|
HasError = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Password != ConfirmPassword)
|
||||||
|
{
|
||||||
|
ErrorMessage = _localizer["Passwords do not match."];
|
||||||
|
HasError = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var changePasswordRequest = new ChangePasswordRequest
|
||||||
|
{
|
||||||
|
NewPassword = Password
|
||||||
|
};
|
||||||
|
|
||||||
|
await _apiClient.Users[_editingUserId].Password.PostAsync(changePasswordRequest);
|
||||||
|
|
||||||
|
CloseDialog();
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error changing password");
|
||||||
|
ErrorMessage = _localizer["Failed to change password."];
|
||||||
|
HasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddRoleAsync()
|
||||||
|
{
|
||||||
|
if(_editingUserId == null || string.IsNullOrWhiteSpace(SelectedRole)) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(UserRoles.Contains(SelectedRole))
|
||||||
|
{
|
||||||
|
ErrorMessage = _localizer["User already has this role."];
|
||||||
|
HasError = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addRoleRequest = new UserRoleRequest
|
||||||
|
{
|
||||||
|
RoleName = SelectedRole
|
||||||
|
};
|
||||||
|
|
||||||
|
await _apiClient.Users[_editingUserId].Roles.PostAsync(addRoleRequest);
|
||||||
|
UserRoles.Add(SelectedRole);
|
||||||
|
|
||||||
|
// Reload users to refresh the list
|
||||||
|
await LoadUsersAsync();
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error adding role");
|
||||||
|
ErrorMessage = _localizer["Failed to add role."];
|
||||||
|
HasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveRoleAsync(string? role)
|
||||||
|
{
|
||||||
|
if(_editingUserId == null || string.IsNullOrWhiteSpace(role)) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _apiClient.Users[_editingUserId].Roles[role].DeleteAsync();
|
||||||
|
UserRoles.Remove(role);
|
||||||
|
|
||||||
|
// Reload users to refresh the list
|
||||||
|
await LoadUsersAsync();
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error removing role");
|
||||||
|
ErrorMessage = _localizer["Failed to remove role."];
|
||||||
|
HasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseDialog()
|
||||||
|
{
|
||||||
|
DialogType = string.Empty;
|
||||||
|
_editingUserId = null;
|
||||||
|
Email = string.Empty;
|
||||||
|
UserName = string.Empty;
|
||||||
|
PhoneNumber = string.Empty;
|
||||||
|
Password = string.Empty;
|
||||||
|
ConfirmPassword = string.Empty;
|
||||||
|
UserRoles.Clear();
|
||||||
|
AvailableRoles.Clear();
|
||||||
|
HasError = false;
|
||||||
|
ErrorMessage = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
184
Marechai.App/Presentation/Views/UsersPage.xaml
Normal file
184
Marechai.App/Presentation/Views/UsersPage.xaml
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<?xml version="1.0"
|
||||||
|
encoding="utf-8"?>
|
||||||
|
|
||||||
|
<Page x:Class="Marechai.App.Presentation.Views.UsersPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
|
||||||
|
x:Name="PageRoot"
|
||||||
|
NavigationCacheMode="Required"
|
||||||
|
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||||
|
|
||||||
|
<Grid RowSpacing="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Header with Title -->
|
||||||
|
<Grid Grid.Row="0"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
|
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="0,0,0,1"
|
||||||
|
Padding="16,12">
|
||||||
|
|
||||||
|
<!-- Page Title -->
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="User Management"
|
||||||
|
FontSize="20"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{ThemeResource TextControlForeground}" />
|
||||||
|
<TextBlock Text="Manage users, roles, and permissions"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource SystemBaseMediumColor}"
|
||||||
|
Margin="0,4,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
|
||||||
|
<!-- Access Denied Message -->
|
||||||
|
<StackPanel Visibility="{Binding IsUberadmin, Converter={StaticResource InvertBoolToVisibilityConverter}}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Padding="24"
|
||||||
|
Spacing="16"
|
||||||
|
MaxWidth="400">
|
||||||
|
<InfoBar IsOpen="True"
|
||||||
|
Severity="Warning"
|
||||||
|
Title="Access Denied"
|
||||||
|
Message="You must be an Uberadmin to access user management."
|
||||||
|
IsClosable="False" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Content (only visible to Uberadmin) -->
|
||||||
|
<Grid Visibility="{Binding IsUberadmin, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||||
|
Canvas.ZIndex="0">
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<StackPanel Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Padding="32"
|
||||||
|
Spacing="16">
|
||||||
|
<ProgressRing IsActive="True"
|
||||||
|
IsIndeterminate="True"
|
||||||
|
Height="64"
|
||||||
|
Width="64"
|
||||||
|
Foreground="{ThemeResource SystemAccentColor}" />
|
||||||
|
<TextBlock Text="Loading users..."
|
||||||
|
FontSize="14"
|
||||||
|
TextAlignment="Center"
|
||||||
|
Foreground="{ThemeResource SystemBaseMediumColor}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<StackPanel Visibility="{Binding HasError, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Padding="24"
|
||||||
|
Spacing="16"
|
||||||
|
MaxWidth="400"
|
||||||
|
Canvas.ZIndex="100">
|
||||||
|
<InfoBar IsOpen="True"
|
||||||
|
Severity="Error"
|
||||||
|
Title="Unable to Load Users"
|
||||||
|
Message="{Binding ErrorMessage}"
|
||||||
|
IsClosable="False" />
|
||||||
|
<Button Content="Retry"
|
||||||
|
Command="{Binding LoadUsersCommand}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Style="{ThemeResource AccentButtonStyle}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Users DataGrid -->
|
||||||
|
<Grid Visibility="{Binding IsDataLoaded, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||||
|
Canvas.ZIndex="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Command Bar -->
|
||||||
|
<CommandBar Grid.Row="0"
|
||||||
|
DefaultLabelPosition="Right">
|
||||||
|
<AppBarButton Icon="Add"
|
||||||
|
Label="Add User"
|
||||||
|
Command="{Binding OpenAddUserDialogCommand}" />
|
||||||
|
<AppBarButton Icon="Refresh"
|
||||||
|
Label="Refresh"
|
||||||
|
Command="{Binding LoadUsersCommand}" />
|
||||||
|
</CommandBar>
|
||||||
|
|
||||||
|
<!-- DataGrid -->
|
||||||
|
<controls:DataGrid Grid.Row="1"
|
||||||
|
Margin="16"
|
||||||
|
ItemsSource="{Binding Users}"
|
||||||
|
SelectedItem="{Binding SelectedUser, Mode=TwoWay}"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
GridLinesVisibility="None"
|
||||||
|
HeadersVisibility="Column"
|
||||||
|
AlternatingRowBackground="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||||
|
CanUserReorderColumns="True"
|
||||||
|
CanUserResizeColumns="True"
|
||||||
|
CanUserSortColumns="True"
|
||||||
|
IsReadOnly="True"
|
||||||
|
SelectionMode="Single"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
|
||||||
|
<controls:DataGrid.Columns>
|
||||||
|
<controls:DataGridTextColumn Header="Email"
|
||||||
|
Binding="{Binding Email}"
|
||||||
|
Width="2*" />
|
||||||
|
|
||||||
|
<controls:DataGridTextColumn Header="Username"
|
||||||
|
Binding="{Binding UserName}"
|
||||||
|
Width="*" />
|
||||||
|
|
||||||
|
<controls:DataGridTextColumn Header="Phone"
|
||||||
|
Binding="{Binding PhoneNumber}"
|
||||||
|
Width="*" />
|
||||||
|
|
||||||
|
<controls:DataGridTextColumn Header="Roles"
|
||||||
|
Width="2*"
|
||||||
|
Binding="{Binding Roles, Converter={StaticResource RolesListConverter}}" />
|
||||||
|
|
||||||
|
<controls:DataGridTemplateColumn Header="Actions"
|
||||||
|
Width="Auto">
|
||||||
|
<controls:DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
Spacing="4">
|
||||||
|
<Button Content="Edit"
|
||||||
|
Command="{Binding DataContext.OpenEditUserDialogCommand, ElementName=PageRoot}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{ThemeResource TextBlockButtonStyle}"
|
||||||
|
Padding="8,4" />
|
||||||
|
<Button Content="Password"
|
||||||
|
Command="{Binding DataContext.OpenChangePasswordDialogCommand, ElementName=PageRoot}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{ThemeResource TextBlockButtonStyle}"
|
||||||
|
Padding="8,4" />
|
||||||
|
<Button Content="Roles"
|
||||||
|
Command="{Binding DataContext.OpenManageRolesDialogCommand, ElementName=PageRoot}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{ThemeResource TextBlockButtonStyle}"
|
||||||
|
Padding="8,4" />
|
||||||
|
<Button Content="Delete"
|
||||||
|
Command="{Binding DataContext.DeleteUserCommand, ElementName=PageRoot}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Foreground="{ThemeResource SystemErrorTextColor}"
|
||||||
|
Style="{ThemeResource TextBlockButtonStyle}"
|
||||||
|
Padding="8,4" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</controls:DataGridTemplateColumn.CellTemplate>
|
||||||
|
</controls:DataGridTemplateColumn>
|
||||||
|
</controls:DataGrid.Columns>
|
||||||
|
</controls:DataGrid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
394
Marechai.App/Presentation/Views/UsersPage.xaml.cs
Normal file
394
Marechai.App/Presentation/Views/UsersPage.xaml.cs
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Marechai.App.Presentation.ViewModels;
|
||||||
|
using Microsoft.UI;
|
||||||
|
using Microsoft.UI.Text;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
|
||||||
|
namespace Marechai.App.Presentation.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User management page for Uberadmins
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class UsersPage : Page
|
||||||
|
{
|
||||||
|
private ContentDialog? _currentOpenDialog;
|
||||||
|
private UsersViewModel? _currentViewModel;
|
||||||
|
|
||||||
|
public UsersPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += UsersPage_Loaded;
|
||||||
|
DataContextChanged += UsersPage_DataContextChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UsersPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||||
|
{
|
||||||
|
// Unsubscribe from previous ViewModel
|
||||||
|
if(_currentViewModel != null) _currentViewModel.ShowDialogRequested -= OnShowDialogRequested;
|
||||||
|
|
||||||
|
// Subscribe to new ViewModel
|
||||||
|
if(DataContext is UsersViewModel vm)
|
||||||
|
{
|
||||||
|
_currentViewModel = vm;
|
||||||
|
vm.ShowDialogRequested += OnShowDialogRequested;
|
||||||
|
|
||||||
|
if(vm.IsUberadmin)
|
||||||
|
{
|
||||||
|
// Load data when DataContext is set and user is Uberadmin
|
||||||
|
vm.LoadUsersCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UsersPage_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if(DataContext is UsersViewModel vm && vm.IsUberadmin)
|
||||||
|
{
|
||||||
|
// Load data when page is loaded (fallback)
|
||||||
|
vm.LoadUsersCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnShowDialogRequested(object? sender, string dialogType)
|
||||||
|
{
|
||||||
|
// Close any currently open dialog first
|
||||||
|
if(_currentOpenDialog != null)
|
||||||
|
{
|
||||||
|
_currentOpenDialog.Hide();
|
||||||
|
_currentOpenDialog = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(DataContext is not UsersViewModel vm) return;
|
||||||
|
|
||||||
|
ContentDialog? dialog = null;
|
||||||
|
|
||||||
|
switch(dialogType)
|
||||||
|
{
|
||||||
|
case "AddEdit":
|
||||||
|
{
|
||||||
|
var emailBox = new TextBox
|
||||||
|
{
|
||||||
|
Header = "Email",
|
||||||
|
Text = vm.Email
|
||||||
|
};
|
||||||
|
|
||||||
|
emailBox.TextChanged += (s, e) => vm.Email = emailBox.Text;
|
||||||
|
|
||||||
|
var userNameBox = new TextBox
|
||||||
|
{
|
||||||
|
Header = "Username",
|
||||||
|
Text = vm.UserName
|
||||||
|
};
|
||||||
|
|
||||||
|
userNameBox.TextChanged += (s, e) => vm.UserName = userNameBox.Text;
|
||||||
|
|
||||||
|
var phoneBox = new TextBox
|
||||||
|
{
|
||||||
|
Header = "Phone Number",
|
||||||
|
Text = vm.PhoneNumber
|
||||||
|
};
|
||||||
|
|
||||||
|
phoneBox.TextChanged += (s, e) => vm.PhoneNumber = phoneBox.Text;
|
||||||
|
|
||||||
|
var passwordBox = new PasswordBox
|
||||||
|
{
|
||||||
|
Header = "Password",
|
||||||
|
Password = vm.Password,
|
||||||
|
PlaceholderText = "Leave blank to keep current (when editing)"
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordBox.PasswordChanged += (s, e) => vm.Password = passwordBox.Password;
|
||||||
|
|
||||||
|
var confirmPasswordBox = new PasswordBox
|
||||||
|
{
|
||||||
|
Header = "Confirm Password",
|
||||||
|
Password = vm.ConfirmPassword
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmPasswordBox.PasswordChanged += (s, e) => vm.ConfirmPassword = confirmPasswordBox.Password;
|
||||||
|
|
||||||
|
dialog = new ContentDialog
|
||||||
|
{
|
||||||
|
XamlRoot = XamlRoot,
|
||||||
|
Title = vm.DialogTitle,
|
||||||
|
PrimaryButtonText = "Save",
|
||||||
|
CloseButtonText = "Cancel",
|
||||||
|
PrimaryButtonCommand = vm.SaveUserCommand,
|
||||||
|
CloseButtonCommand = vm.CloseDialogCommand,
|
||||||
|
DefaultButton = ContentDialogButton.Primary,
|
||||||
|
Content = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 12,
|
||||||
|
MinWidth = 400,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
emailBox,
|
||||||
|
userNameBox,
|
||||||
|
phoneBox,
|
||||||
|
passwordBox,
|
||||||
|
confirmPasswordBox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Password":
|
||||||
|
{
|
||||||
|
var passwordBox = new PasswordBox
|
||||||
|
{
|
||||||
|
Header = "New Password",
|
||||||
|
Password = vm.Password
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordBox.PasswordChanged += (s, e) => vm.Password = passwordBox.Password;
|
||||||
|
|
||||||
|
var confirmPasswordBox = new PasswordBox
|
||||||
|
{
|
||||||
|
Header = "Confirm Password",
|
||||||
|
Password = vm.ConfirmPassword
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmPasswordBox.PasswordChanged += (s, e) => vm.ConfirmPassword = confirmPasswordBox.Password;
|
||||||
|
|
||||||
|
dialog = new ContentDialog
|
||||||
|
{
|
||||||
|
XamlRoot = XamlRoot,
|
||||||
|
Title = vm.DialogTitle,
|
||||||
|
PrimaryButtonText = "Change Password",
|
||||||
|
CloseButtonText = "Cancel",
|
||||||
|
PrimaryButtonCommand = vm.SavePasswordCommand,
|
||||||
|
CloseButtonCommand = vm.CloseDialogCommand,
|
||||||
|
DefaultButton = ContentDialogButton.Primary,
|
||||||
|
Content = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 12,
|
||||||
|
MinWidth = 400,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
passwordBox,
|
||||||
|
confirmPasswordBox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Roles":
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Creating Roles dialog. Available roles count: {vm.AvailableRoles.Count}");
|
||||||
|
foreach(string role in vm.AvailableRoles) Debug.WriteLine($" - Role: {role}");
|
||||||
|
|
||||||
|
var rolesContent = new Grid
|
||||||
|
{
|
||||||
|
RowSpacing = 16,
|
||||||
|
MinWidth = 450
|
||||||
|
};
|
||||||
|
|
||||||
|
rolesContent.RowDefinitions.Add(new RowDefinition
|
||||||
|
{
|
||||||
|
Height = GridLength.Auto
|
||||||
|
});
|
||||||
|
|
||||||
|
rolesContent.RowDefinitions.Add(new RowDefinition
|
||||||
|
{
|
||||||
|
Height = new GridLength(1, GridUnitType.Star)
|
||||||
|
});
|
||||||
|
|
||||||
|
var addRolePanel = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
addRolePanel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "Add Role",
|
||||||
|
FontWeight = FontWeights.SemiBold
|
||||||
|
});
|
||||||
|
|
||||||
|
var addRoleGrid = new Grid
|
||||||
|
{
|
||||||
|
ColumnSpacing = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
addRoleGrid.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
|
{
|
||||||
|
Width = new GridLength(1, GridUnitType.Star)
|
||||||
|
});
|
||||||
|
|
||||||
|
addRoleGrid.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
|
{
|
||||||
|
Width = GridLength.Auto
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use ListView with SingleSelection instead of ComboBox - ComboBox has issues with programmatic creation
|
||||||
|
var roleListView = new ListView
|
||||||
|
{
|
||||||
|
SelectionMode = ListViewSelectionMode.Single,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
MaxHeight = 200,
|
||||||
|
MinWidth = 300
|
||||||
|
};
|
||||||
|
|
||||||
|
Debug.WriteLine($"Creating ListView for role selection with {vm.AvailableRoles.Count} roles");
|
||||||
|
|
||||||
|
// Populate the ListView
|
||||||
|
foreach(string role in vm.AvailableRoles)
|
||||||
|
{
|
||||||
|
var item = new ListViewItem
|
||||||
|
{
|
||||||
|
Content = role
|
||||||
|
};
|
||||||
|
|
||||||
|
roleListView.Items.Add(item);
|
||||||
|
Debug.WriteLine($" Added role to ListView: {role}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"ListView Items.Count: {roleListView.Items.Count}");
|
||||||
|
|
||||||
|
roleListView.SelectionChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if(roleListView.SelectedItem is ListViewItem selectedItem &&
|
||||||
|
selectedItem.Content is string selectedRole)
|
||||||
|
{
|
||||||
|
vm.SelectedRole = selectedRole;
|
||||||
|
Debug.WriteLine($"Selected role from ListView: {selectedRole}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Grid.SetColumn(roleListView, 0);
|
||||||
|
addRoleGrid.Children.Add(roleListView);
|
||||||
|
|
||||||
|
var addButton = new Button
|
||||||
|
{
|
||||||
|
Content = "Add",
|
||||||
|
Command = vm.AddRoleCommand
|
||||||
|
};
|
||||||
|
|
||||||
|
Grid.SetColumn(addButton, 1);
|
||||||
|
addRoleGrid.Children.Add(addButton);
|
||||||
|
|
||||||
|
addRolePanel.Children.Add(addRoleGrid);
|
||||||
|
Grid.SetRow(addRolePanel, 0);
|
||||||
|
rolesContent.Children.Add(addRolePanel);
|
||||||
|
|
||||||
|
var currentRolesPanel = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 8,
|
||||||
|
Margin = new Thickness(0, 8, 0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
currentRolesPanel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "Current Roles",
|
||||||
|
FontWeight = FontWeights.SemiBold
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a StackPanel to display roles dynamically
|
||||||
|
var rolesStack = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler to refresh the roles list
|
||||||
|
void UpdateRolesList()
|
||||||
|
{
|
||||||
|
rolesStack.Children.Clear();
|
||||||
|
|
||||||
|
foreach(string role in vm.UserRoles)
|
||||||
|
{
|
||||||
|
var roleItem = new Grid
|
||||||
|
{
|
||||||
|
ColumnSpacing = 8,
|
||||||
|
Margin = new Thickness(0, 4, 0, 4)
|
||||||
|
};
|
||||||
|
|
||||||
|
roleItem.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
|
{
|
||||||
|
Width = new GridLength(1, GridUnitType.Star)
|
||||||
|
});
|
||||||
|
|
||||||
|
roleItem.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
|
{
|
||||||
|
Width = GridLength.Auto
|
||||||
|
});
|
||||||
|
|
||||||
|
var roleText = new TextBlock
|
||||||
|
{
|
||||||
|
Text = role,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
Grid.SetColumn(roleText, 0);
|
||||||
|
roleItem.Children.Add(roleText);
|
||||||
|
|
||||||
|
var removeButton = new Button
|
||||||
|
{
|
||||||
|
Content = "Remove",
|
||||||
|
Foreground = new SolidColorBrush(Colors.Red),
|
||||||
|
CommandParameter = role
|
||||||
|
};
|
||||||
|
|
||||||
|
removeButton.Click += (s, e) =>
|
||||||
|
{
|
||||||
|
var btn = s as Button;
|
||||||
|
var roleToRemove = btn?.CommandParameter as string;
|
||||||
|
|
||||||
|
if(roleToRemove != null && vm.RemoveRoleCommand.CanExecute(roleToRemove))
|
||||||
|
{
|
||||||
|
vm.RemoveRoleCommand.Execute(roleToRemove);
|
||||||
|
UpdateRolesList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Grid.SetColumn(removeButton, 1);
|
||||||
|
roleItem.Children.Add(removeButton);
|
||||||
|
|
||||||
|
rolesStack.Children.Add(roleItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to collection changes
|
||||||
|
vm.UserRoles.CollectionChanged += (s, e) => UpdateRolesList();
|
||||||
|
|
||||||
|
// Initial population
|
||||||
|
UpdateRolesList();
|
||||||
|
|
||||||
|
var scrollViewer = new ScrollViewer
|
||||||
|
{
|
||||||
|
MaxHeight = 200,
|
||||||
|
Content = rolesStack
|
||||||
|
};
|
||||||
|
|
||||||
|
currentRolesPanel.Children.Add(scrollViewer);
|
||||||
|
|
||||||
|
Grid.SetRow(currentRolesPanel, 1);
|
||||||
|
rolesContent.Children.Add(currentRolesPanel);
|
||||||
|
|
||||||
|
dialog = new ContentDialog
|
||||||
|
{
|
||||||
|
XamlRoot = XamlRoot,
|
||||||
|
Title = vm.DialogTitle,
|
||||||
|
CloseButtonText = "Close",
|
||||||
|
CloseButtonCommand = vm.CloseDialogCommand,
|
||||||
|
DefaultButton = ContentDialogButton.Close,
|
||||||
|
Content = rolesContent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dialog != null)
|
||||||
|
{
|
||||||
|
_currentOpenDialog = dialog;
|
||||||
|
await dialog.ShowAsync();
|
||||||
|
_currentOpenDialog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
Marechai.App/Services/Authentication/JwtService.cs
Normal file
112
Marechai.App/Services/Authentication/JwtService.cs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace Marechai.App.Services.Authentication;
|
||||||
|
|
||||||
|
public interface IJwtService
|
||||||
|
{
|
||||||
|
IEnumerable<string> GetRoles(string token);
|
||||||
|
string? GetUserId(string token);
|
||||||
|
string? GetUserName(string token);
|
||||||
|
string? GetEmail(string token);
|
||||||
|
bool IsTokenValid(string token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class JwtService : IJwtService
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<string> GetRoles(string token)
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(token)) return [];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
JwtSecurityToken jwtToken = handler.ReadJwtToken(token);
|
||||||
|
|
||||||
|
return jwtToken.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string? GetUserId(string token)
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(token)) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
JwtSecurityToken jwtToken = handler.ReadJwtToken(token);
|
||||||
|
|
||||||
|
return jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Sid)?.Value;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string? GetUserName(string token)
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(token)) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
JwtSecurityToken jwtToken = handler.ReadJwtToken(token);
|
||||||
|
|
||||||
|
return jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string? GetEmail(string token)
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(token)) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
JwtSecurityToken jwtToken = handler.ReadJwtToken(token);
|
||||||
|
|
||||||
|
return jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsTokenValid(string token)
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(token)) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
JwtSecurityToken jwtToken = handler.ReadJwtToken(token);
|
||||||
|
|
||||||
|
// Check if token has expired (if expiration is set)
|
||||||
|
if(jwtToken.ValidTo != DateTime.MinValue) return jwtToken.ValidTo > DateTime.UtcNow;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user