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:
Dev
2026-04-09 15:17:53 +02:00
parent 69c9d77be3
commit 42b5eda460
4 changed files with 139 additions and 1 deletions

View File

@@ -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;
}
}
}