mirror of
https://github.com/claunia/marechai.git
synced 2025-12-16 11:04: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.Authentication.JwtBearer" 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 Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
|
||||
<!-- Add more community toolkit references here -->
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
||||
<PackageVersion Include="Uno.CommunityToolkit.WinUI.UI.Controls" Version="7.1.200" />
|
||||
<!-- Add more uno community toolkit references here -->
|
||||
</ItemGroup>
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2"/>
|
||||
<!-- Add more community toolkit references here -->
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
||||
<PackageVersion Include="Uno.CommunityToolkit.WinUI.UI.Controls" Version="7.1.200"/>
|
||||
<!-- Add more uno community toolkit references here -->
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -21,6 +21,7 @@
|
||||
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<local:InvertBoolToVisibilityConverter x:Key="InvertBoolToVisibilityConverter" />
|
||||
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
<local:RolesListConverter x:Key="RolesListConverter" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
@@ -129,6 +129,7 @@ public partial class App : Application
|
||||
AuthService>();
|
||||
|
||||
services.AddSingleton<ITokenService, TokenService>();
|
||||
services.AddSingleton<IJwtService, JwtService>();
|
||||
services.AddSingleton<FlagCache>();
|
||||
services.AddSingleton<CompanyLogoCache>();
|
||||
services.AddSingleton<MachinePhotoCache>();
|
||||
@@ -200,6 +201,7 @@ public partial class App : Application
|
||||
new ViewMap<SoundSynthListPage, SoundSynthsListViewModel>(),
|
||||
new ViewMap<SoundSynthDetailPage, SoundSynthDetailViewModel>(),
|
||||
new ViewMap<SettingsPage, SettingsViewModel>(),
|
||||
new ViewMap<UsersPage, UsersViewModel>(),
|
||||
new DataViewMap<SecondPage, SecondViewModel, Entity>());
|
||||
|
||||
routes.Register(new RouteMap("",
|
||||
@@ -247,7 +249,8 @@ public partial class App : Application
|
||||
]),
|
||||
new RouteMap("gpus",
|
||||
views.FindByViewModel<GpuListViewModel>(),
|
||||
Nested:
|
||||
Nested
|
||||
:
|
||||
[
|
||||
new RouteMap("gpu-details",
|
||||
views.FindByViewModel<
|
||||
@@ -275,6 +278,8 @@ public partial class App : Application
|
||||
]),
|
||||
new RouteMap("settings",
|
||||
views.FindByViewModel<SettingsViewModel>()),
|
||||
new RouteMap("users",
|
||||
views.FindByViewModel<UsersViewModel>()),
|
||||
new RouteMap("Second",
|
||||
views.FindByViewModel<SecondViewModel>())
|
||||
])
|
||||
|
||||
@@ -41,10 +41,11 @@
|
||||
</UnoFeatures>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Marechai.Data\Marechai.Data.csproj" />
|
||||
<ProjectReference Include="..\Marechai.Data\Marechai.Data.csproj"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Humanizer" />
|
||||
<PackageReference Include="Humanizer"/>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Presentation\Views\Shell.xaml.cs">
|
||||
@@ -79,16 +80,20 @@
|
||||
<DependentUpon>Sidebar.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
<Compile Update="Presentation\Views\UsersPage.xaml.cs">
|
||||
<DependentUpon>UsersPage.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" />
|
||||
<!-- Add more community toolkit references here -->
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
||||
<PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls" />
|
||||
<!-- Add more uno community toolkit references here -->
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls"/>
|
||||
<!-- Add more community toolkit references here -->
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
||||
<PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls"/>
|
||||
<!-- Add more uno community toolkit references here -->
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -231,6 +231,19 @@
|
||||
Visibility="{Binding IsSidebarOpen}">
|
||||
<StackPanel Orientation="Vertical"
|
||||
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 -->
|
||||
<Button Content="{Binding LoginLogoutButtonText}"
|
||||
Command="{Binding LoginLogoutCommand}"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Marechai.App.Services;
|
||||
using Marechai.App.Services.Authentication;
|
||||
using Uno.Extensions.Authentication;
|
||||
using Uno.Extensions.Navigation;
|
||||
using Uno.Extensions.Toolkit;
|
||||
@@ -13,11 +15,15 @@ namespace Marechai.App.Presentation.ViewModels;
|
||||
public partial class MainViewModel : ObservableObject
|
||||
{
|
||||
private readonly IAuthenticationService _authService;
|
||||
private readonly IJwtService _jwtService;
|
||||
private readonly IStringLocalizer _localizer;
|
||||
private readonly INavigator _navigator;
|
||||
private readonly ITokenService _tokenService;
|
||||
[ObservableProperty]
|
||||
private bool _isSidebarOpen = true;
|
||||
[ObservableProperty]
|
||||
private bool _isUberadminUser;
|
||||
[ObservableProperty]
|
||||
private Dictionary<string, string> _localizedStrings = new();
|
||||
[ObservableProperty]
|
||||
private string _loginLogoutButtonText = "";
|
||||
@@ -31,11 +37,13 @@ public partial class MainViewModel : ObservableObject
|
||||
|
||||
public MainViewModel(IStringLocalizer localizer, IOptions<AppConfig> appInfo, INavigator navigator,
|
||||
NewsViewModel newsViewModel, IColorThemeService colorThemeService, IThemeService themeService,
|
||||
IAuthenticationService authService)
|
||||
IAuthenticationService authService, IJwtService jwtService, ITokenService tokenService)
|
||||
{
|
||||
_navigator = navigator;
|
||||
_localizer = localizer;
|
||||
_authService = authService;
|
||||
_jwtService = jwtService;
|
||||
_tokenService = tokenService;
|
||||
NewsViewModel = newsViewModel;
|
||||
Title = "Marechai";
|
||||
Title += $" - {localizer["ApplicationName"]}";
|
||||
@@ -63,6 +71,7 @@ public partial class MainViewModel : ObservableObject
|
||||
NavigateToProcessorsCommand = new AsyncRelayCommand(() => NavigateTo("processors"));
|
||||
NavigateToSoftwareCommand = new AsyncRelayCommand(() => NavigateTo("software"));
|
||||
NavigateToSoundSynthesizersCommand = new AsyncRelayCommand(() => NavigateTo("sound-synths"));
|
||||
NavigateToUsersCommand = new AsyncRelayCommand(() => NavigateTo("users"));
|
||||
NavigateToSettingsCommand = new AsyncRelayCommand(() => NavigateTo("settings"));
|
||||
LoginLogoutCommand = new RelayCommand(HandleLoginLogout);
|
||||
ToggleSidebarCommand = new RelayCommand(() => IsSidebarOpen = !IsSidebarOpen);
|
||||
@@ -71,6 +80,7 @@ public partial class MainViewModel : ObservableObject
|
||||
_authService.LoggedOut += OnLoggedOut;
|
||||
|
||||
UpdateLoginLogoutButtonText();
|
||||
UpdateUberadminStatus();
|
||||
}
|
||||
|
||||
public string? Title { get; }
|
||||
@@ -90,6 +100,7 @@ public partial class MainViewModel : ObservableObject
|
||||
public ICommand NavigateToProcessorsCommand { get; }
|
||||
public ICommand NavigateToSoftwareCommand { get; }
|
||||
public ICommand NavigateToSoundSynthesizersCommand { get; }
|
||||
public ICommand NavigateToUsersCommand { get; }
|
||||
public ICommand NavigateToSettingsCommand { get; }
|
||||
public ICommand LoginLogoutCommand { get; }
|
||||
public ICommand ToggleSidebarCommand { get; }
|
||||
@@ -171,16 +182,38 @@ public partial class MainViewModel : ObservableObject
|
||||
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)
|
||||
{
|
||||
// Update button text when user logs out
|
||||
UpdateLoginLogoutButtonText();
|
||||
UpdateUberadminStatus();
|
||||
}
|
||||
|
||||
public void RefreshAuthenticationState()
|
||||
{
|
||||
// Public method to refresh authentication state (called after login)
|
||||
UpdateLoginLogoutButtonText();
|
||||
UpdateUberadminStatus();
|
||||
}
|
||||
|
||||
private async void HandleLoginLogout()
|
||||
@@ -192,6 +225,7 @@ public partial class MainViewModel : ObservableObject
|
||||
// Logout
|
||||
await _authService.LogoutAsync(null, CancellationToken.None);
|
||||
UpdateLoginLogoutButtonText();
|
||||
UpdateUberadminStatus();
|
||||
}
|
||||
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