feat(19-02): add RegisterApp/RemoveApp commands, DI wiring, EN/FR localization
- ProfileManagementViewModel: IAppRegistrationService injected, RegisterAppCommand/RemoveAppCommand added - IsRegistering, ShowFallbackInstructions, RegistrationStatus observable properties - HasRegisteredApp computed property, CanRegisterApp/CanRemoveApp guards - RegisterAppAsync: admin check, fallback panel, AppId persistence - RemoveAppAsync: removal + MSAL clear + AppId null + persistence - App.xaml.cs: IAppRegistrationService singleton registered - Strings.resx/fr.resx: 16 new localization keys for register/remove/fallback flow
This commit is contained in:
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Win32;
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Localization;
|
||||
using SharepointToolbox.Services;
|
||||
using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory;
|
||||
|
||||
@@ -16,6 +17,7 @@ public partial class ProfileManagementViewModel : ObservableObject
|
||||
private readonly IBrandingService _brandingService;
|
||||
private readonly AppGraphClientFactory _graphClientFactory;
|
||||
private readonly ILogger<ProfileManagementViewModel> _logger;
|
||||
private readonly IAppRegistrationService _appRegistrationService;
|
||||
|
||||
[ObservableProperty]
|
||||
private TenantProfile? _selectedProfile;
|
||||
@@ -32,6 +34,17 @@ public partial class ProfileManagementViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private string _validationMessage = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isRegistering;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showFallbackInstructions;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _registrationStatus = string.Empty;
|
||||
|
||||
public bool HasRegisteredApp => SelectedProfile?.AppId != null;
|
||||
|
||||
private string? _clientLogoPreview;
|
||||
public string? ClientLogoPreview
|
||||
{
|
||||
@@ -47,17 +60,21 @@ public partial class ProfileManagementViewModel : ObservableObject
|
||||
public IAsyncRelayCommand BrowseClientLogoCommand { get; }
|
||||
public IAsyncRelayCommand ClearClientLogoCommand { get; }
|
||||
public IAsyncRelayCommand AutoPullClientLogoCommand { get; }
|
||||
public IAsyncRelayCommand RegisterAppCommand { get; }
|
||||
public IAsyncRelayCommand RemoveAppCommand { get; }
|
||||
|
||||
public ProfileManagementViewModel(
|
||||
ProfileService profileService,
|
||||
IBrandingService brandingService,
|
||||
AppGraphClientFactory graphClientFactory,
|
||||
ILogger<ProfileManagementViewModel> logger)
|
||||
ILogger<ProfileManagementViewModel> logger,
|
||||
IAppRegistrationService appRegistrationService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
_brandingService = brandingService;
|
||||
_graphClientFactory = graphClientFactory;
|
||||
_logger = logger;
|
||||
_appRegistrationService = appRegistrationService;
|
||||
|
||||
AddCommand = new AsyncRelayCommand(AddAsync, CanAdd);
|
||||
RenameCommand = new AsyncRelayCommand(RenameAsync, () => SelectedProfile != null && !string.IsNullOrWhiteSpace(NewName));
|
||||
@@ -65,6 +82,8 @@ public partial class ProfileManagementViewModel : ObservableObject
|
||||
BrowseClientLogoCommand = new AsyncRelayCommand(BrowseClientLogoAsync, () => SelectedProfile != null);
|
||||
ClearClientLogoCommand = new AsyncRelayCommand(ClearClientLogoAsync, () => SelectedProfile != null);
|
||||
AutoPullClientLogoCommand = new AsyncRelayCommand(AutoPullClientLogoAsync, () => SelectedProfile != null);
|
||||
RegisterAppCommand = new AsyncRelayCommand(RegisterAppAsync, CanRegisterApp);
|
||||
RemoveAppCommand = new AsyncRelayCommand(RemoveAppAsync, CanRemoveApp);
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
@@ -94,6 +113,15 @@ public partial class ProfileManagementViewModel : ObservableObject
|
||||
AutoPullClientLogoCommand.NotifyCanExecuteChanged();
|
||||
RenameCommand.NotifyCanExecuteChanged();
|
||||
DeleteCommand.NotifyCanExecuteChanged();
|
||||
OnPropertyChanged(nameof(HasRegisteredApp));
|
||||
RegisterAppCommand.NotifyCanExecuteChanged();
|
||||
RemoveAppCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
partial void OnIsRegisteringChanged(bool value)
|
||||
{
|
||||
RegisterAppCommand.NotifyCanExecuteChanged();
|
||||
RemoveAppCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private static string? FormatLogoPreview(LogoData? logo)
|
||||
@@ -256,4 +284,77 @@ public partial class ProfileManagementViewModel : ObservableObject
|
||||
_logger.LogWarning(ex, "Auto-pull client logo failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanRegisterApp()
|
||||
=> SelectedProfile != null && SelectedProfile.AppId == null && !IsRegistering;
|
||||
|
||||
private bool CanRemoveApp()
|
||||
=> SelectedProfile != null && SelectedProfile.AppId != null && !IsRegistering;
|
||||
|
||||
private async Task RegisterAppAsync(CancellationToken ct)
|
||||
{
|
||||
if (SelectedProfile == null) return;
|
||||
IsRegistering = true;
|
||||
ShowFallbackInstructions = false;
|
||||
RegistrationStatus = TranslationSource.Instance["profile.register.checking"];
|
||||
try
|
||||
{
|
||||
var isAdmin = await _appRegistrationService.IsGlobalAdminAsync(SelectedProfile.ClientId, ct);
|
||||
if (!isAdmin)
|
||||
{
|
||||
ShowFallbackInstructions = true;
|
||||
RegistrationStatus = TranslationSource.Instance["profile.register.noperm"];
|
||||
return;
|
||||
}
|
||||
|
||||
RegistrationStatus = TranslationSource.Instance["profile.register.registering"];
|
||||
var result = await _appRegistrationService.RegisterAsync(SelectedProfile.ClientId, SelectedProfile.Name, ct);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
SelectedProfile.AppId = result.AppId;
|
||||
await _profileService.UpdateProfileAsync(SelectedProfile);
|
||||
RegistrationStatus = TranslationSource.Instance["profile.register.success"];
|
||||
OnPropertyChanged(nameof(HasRegisteredApp));
|
||||
}
|
||||
else
|
||||
{
|
||||
RegistrationStatus = result.ErrorMessage ?? TranslationSource.Instance["profile.register.failed"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RegistrationStatus = ex.Message;
|
||||
_logger.LogError(ex, "Failed to register application.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRegistering = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveAppAsync(CancellationToken ct)
|
||||
{
|
||||
if (SelectedProfile == null) return;
|
||||
IsRegistering = true;
|
||||
RegistrationStatus = TranslationSource.Instance["profile.remove.removing"];
|
||||
try
|
||||
{
|
||||
await _appRegistrationService.RemoveAsync(SelectedProfile.ClientId, SelectedProfile.AppId!, ct);
|
||||
await _appRegistrationService.ClearMsalSessionAsync(SelectedProfile.ClientId, SelectedProfile.TenantUrl);
|
||||
SelectedProfile.AppId = null;
|
||||
await _profileService.UpdateProfileAsync(SelectedProfile);
|
||||
RegistrationStatus = TranslationSource.Instance["profile.remove.success"];
|
||||
OnPropertyChanged(nameof(HasRegisteredApp));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RegistrationStatus = ex.Message;
|
||||
_logger.LogError(ex, "Failed to remove application.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRegistering = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user