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:
@@ -164,6 +164,9 @@ public partial class App : Application
|
|||||||
// Phase 18: Auto-Take Ownership
|
// Phase 18: Auto-Take Ownership
|
||||||
services.AddTransient<IOwnershipElevationService, OwnershipElevationService>();
|
services.AddTransient<IOwnershipElevationService, OwnershipElevationService>();
|
||||||
|
|
||||||
|
// Phase 19: App Registration & Removal
|
||||||
|
services.AddSingleton<IAppRegistrationService, AppRegistrationService>();
|
||||||
|
|
||||||
// Phase 7: User Access Audit
|
// Phase 7: User Access Audit
|
||||||
services.AddTransient<IUserAccessAuditService, UserAccessAuditService>();
|
services.AddTransient<IUserAccessAuditService, UserAccessAuditService>();
|
||||||
services.AddTransient<IGraphUserSearchService, GraphUserSearchService>();
|
services.AddTransient<IGraphUserSearchService, GraphUserSearchService>();
|
||||||
|
|||||||
@@ -412,6 +412,23 @@
|
|||||||
<!-- Phase 16: Report Consolidation Toggle -->
|
<!-- Phase 16: Report Consolidation Toggle -->
|
||||||
<data name="audit.grp.export" xml:space="preserve"><value>Options d'exportation</value></data>
|
<data name="audit.grp.export" xml:space="preserve"><value>Options d'exportation</value></data>
|
||||||
<data name="chk.merge.permissions" xml:space="preserve"><value>Fusionner les permissions en double</value></data>
|
<data name="chk.merge.permissions" xml:space="preserve"><value>Fusionner les permissions en double</value></data>
|
||||||
|
<!-- Phase 19: App Registration & Removal -->
|
||||||
|
<data name="profile.register" xml:space="preserve"><value>Enregistrer l'app</value></data>
|
||||||
|
<data name="profile.remove" xml:space="preserve"><value>Supprimer l'app</value></data>
|
||||||
|
<data name="profile.register.checking" xml:space="preserve"><value>Vérification des permissions...</value></data>
|
||||||
|
<data name="profile.register.registering" xml:space="preserve"><value>Enregistrement de l'application...</value></data>
|
||||||
|
<data name="profile.register.success" xml:space="preserve"><value>Application enregistrée avec succès</value></data>
|
||||||
|
<data name="profile.register.failed" xml:space="preserve"><value>L'enregistrement a échoué</value></data>
|
||||||
|
<data name="profile.register.noperm" xml:space="preserve"><value>Permissions insuffisantes pour l'enregistrement automatique</value></data>
|
||||||
|
<data name="profile.remove.removing" xml:space="preserve"><value>Suppression de l'application...</value></data>
|
||||||
|
<data name="profile.remove.success" xml:space="preserve"><value>Application supprimée avec succès</value></data>
|
||||||
|
<data name="profile.fallback.title" xml:space="preserve"><value>Enregistrement manuel requis</value></data>
|
||||||
|
<data name="profile.fallback.step1" xml:space="preserve"><value>1. Allez dans le portail Azure > Inscriptions d'applications > Nouvelle inscription</value></data>
|
||||||
|
<data name="profile.fallback.step2" xml:space="preserve"><value>2. Nom : 'SharePoint Toolbox - {0}', Types de comptes : Locataire unique</value></data>
|
||||||
|
<data name="profile.fallback.step3" xml:space="preserve"><value>3. URI de redirection : Client public, https://login.microsoftonline.com/common/oauth2/nativeclient</value></data>
|
||||||
|
<data name="profile.fallback.step4" xml:space="preserve"><value>4. Sous Permissions API, ajouter : Microsoft Graph (User.Read, User.Read.All, Group.Read.All, Directory.Read.All) et SharePoint (AllSites.FullControl)</value></data>
|
||||||
|
<data name="profile.fallback.step5" xml:space="preserve"><value>5. Accorder le consentement administrateur pour toutes les permissions</value></data>
|
||||||
|
<data name="profile.fallback.step6" xml:space="preserve"><value>6. Copier l'ID d'application (client) et le coller dans le champ ID Client ci-dessus</value></data>
|
||||||
<!-- Phase 18: Auto-Take Ownership -->
|
<!-- Phase 18: Auto-Take Ownership -->
|
||||||
<data name="settings.ownership.title" xml:space="preserve"><value>Propriété du site</value></data>
|
<data name="settings.ownership.title" xml:space="preserve"><value>Propriété du site</value></data>
|
||||||
<data name="settings.ownership.auto" xml:space="preserve"><value>Prendre automatiquement la propriété d'administrateur de collection de sites en cas de refus d'accès</value></data>
|
<data name="settings.ownership.auto" xml:space="preserve"><value>Prendre automatiquement la propriété d'administrateur de collection de sites en cas de refus d'accès</value></data>
|
||||||
|
|||||||
@@ -412,6 +412,23 @@
|
|||||||
<!-- Phase 16: Report Consolidation Toggle -->
|
<!-- Phase 16: Report Consolidation Toggle -->
|
||||||
<data name="audit.grp.export" xml:space="preserve"><value>Export Options</value></data>
|
<data name="audit.grp.export" xml:space="preserve"><value>Export Options</value></data>
|
||||||
<data name="chk.merge.permissions" xml:space="preserve"><value>Merge duplicate permissions</value></data>
|
<data name="chk.merge.permissions" xml:space="preserve"><value>Merge duplicate permissions</value></data>
|
||||||
|
<!-- Phase 19: App Registration & Removal -->
|
||||||
|
<data name="profile.register" xml:space="preserve"><value>Register App</value></data>
|
||||||
|
<data name="profile.remove" xml:space="preserve"><value>Remove App</value></data>
|
||||||
|
<data name="profile.register.checking" xml:space="preserve"><value>Checking permissions...</value></data>
|
||||||
|
<data name="profile.register.registering" xml:space="preserve"><value>Registering application...</value></data>
|
||||||
|
<data name="profile.register.success" xml:space="preserve"><value>Application registered successfully</value></data>
|
||||||
|
<data name="profile.register.failed" xml:space="preserve"><value>Registration failed</value></data>
|
||||||
|
<data name="profile.register.noperm" xml:space="preserve"><value>Insufficient permissions for automatic registration</value></data>
|
||||||
|
<data name="profile.remove.removing" xml:space="preserve"><value>Removing application...</value></data>
|
||||||
|
<data name="profile.remove.success" xml:space="preserve"><value>Application removed successfully</value></data>
|
||||||
|
<data name="profile.fallback.title" xml:space="preserve"><value>Manual Registration Required</value></data>
|
||||||
|
<data name="profile.fallback.step1" xml:space="preserve"><value>1. Go to Azure Portal > App registrations > New registration</value></data>
|
||||||
|
<data name="profile.fallback.step2" xml:space="preserve"><value>2. Name: 'SharePoint Toolbox - {0}', Supported account types: Single tenant</value></data>
|
||||||
|
<data name="profile.fallback.step3" xml:space="preserve"><value>3. Redirect URI: Public client, https://login.microsoftonline.com/common/oauth2/nativeclient</value></data>
|
||||||
|
<data name="profile.fallback.step4" xml:space="preserve"><value>4. Under API permissions, add: Microsoft Graph (User.Read, User.Read.All, Group.Read.All, Directory.Read.All) and SharePoint (AllSites.FullControl)</value></data>
|
||||||
|
<data name="profile.fallback.step5" xml:space="preserve"><value>5. Grant admin consent for all permissions</value></data>
|
||||||
|
<data name="profile.fallback.step6" xml:space="preserve"><value>6. Copy the Application (client) ID and paste it in the Client ID field above</value></data>
|
||||||
<!-- Phase 18: Auto-Take Ownership -->
|
<!-- Phase 18: Auto-Take Ownership -->
|
||||||
<data name="settings.ownership.title" xml:space="preserve"><value>Site Ownership</value></data>
|
<data name="settings.ownership.title" xml:space="preserve"><value>Site Ownership</value></data>
|
||||||
<data name="settings.ownership.auto" xml:space="preserve"><value>Automatically take site collection admin ownership on access denied</value></data>
|
<data name="settings.ownership.auto" xml:space="preserve"><value>Automatically take site collection admin ownership on access denied</value></data>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using SharepointToolbox.Core.Models;
|
using SharepointToolbox.Core.Models;
|
||||||
|
using SharepointToolbox.Localization;
|
||||||
using SharepointToolbox.Services;
|
using SharepointToolbox.Services;
|
||||||
using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory;
|
using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory;
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ public partial class ProfileManagementViewModel : ObservableObject
|
|||||||
private readonly IBrandingService _brandingService;
|
private readonly IBrandingService _brandingService;
|
||||||
private readonly AppGraphClientFactory _graphClientFactory;
|
private readonly AppGraphClientFactory _graphClientFactory;
|
||||||
private readonly ILogger<ProfileManagementViewModel> _logger;
|
private readonly ILogger<ProfileManagementViewModel> _logger;
|
||||||
|
private readonly IAppRegistrationService _appRegistrationService;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private TenantProfile? _selectedProfile;
|
private TenantProfile? _selectedProfile;
|
||||||
@@ -32,6 +34,17 @@ public partial class ProfileManagementViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _validationMessage = string.Empty;
|
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;
|
private string? _clientLogoPreview;
|
||||||
public string? ClientLogoPreview
|
public string? ClientLogoPreview
|
||||||
{
|
{
|
||||||
@@ -47,17 +60,21 @@ public partial class ProfileManagementViewModel : ObservableObject
|
|||||||
public IAsyncRelayCommand BrowseClientLogoCommand { get; }
|
public IAsyncRelayCommand BrowseClientLogoCommand { get; }
|
||||||
public IAsyncRelayCommand ClearClientLogoCommand { get; }
|
public IAsyncRelayCommand ClearClientLogoCommand { get; }
|
||||||
public IAsyncRelayCommand AutoPullClientLogoCommand { get; }
|
public IAsyncRelayCommand AutoPullClientLogoCommand { get; }
|
||||||
|
public IAsyncRelayCommand RegisterAppCommand { get; }
|
||||||
|
public IAsyncRelayCommand RemoveAppCommand { get; }
|
||||||
|
|
||||||
public ProfileManagementViewModel(
|
public ProfileManagementViewModel(
|
||||||
ProfileService profileService,
|
ProfileService profileService,
|
||||||
IBrandingService brandingService,
|
IBrandingService brandingService,
|
||||||
AppGraphClientFactory graphClientFactory,
|
AppGraphClientFactory graphClientFactory,
|
||||||
ILogger<ProfileManagementViewModel> logger)
|
ILogger<ProfileManagementViewModel> logger,
|
||||||
|
IAppRegistrationService appRegistrationService)
|
||||||
{
|
{
|
||||||
_profileService = profileService;
|
_profileService = profileService;
|
||||||
_brandingService = brandingService;
|
_brandingService = brandingService;
|
||||||
_graphClientFactory = graphClientFactory;
|
_graphClientFactory = graphClientFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_appRegistrationService = appRegistrationService;
|
||||||
|
|
||||||
AddCommand = new AsyncRelayCommand(AddAsync, CanAdd);
|
AddCommand = new AsyncRelayCommand(AddAsync, CanAdd);
|
||||||
RenameCommand = new AsyncRelayCommand(RenameAsync, () => SelectedProfile != null && !string.IsNullOrWhiteSpace(NewName));
|
RenameCommand = new AsyncRelayCommand(RenameAsync, () => SelectedProfile != null && !string.IsNullOrWhiteSpace(NewName));
|
||||||
@@ -65,6 +82,8 @@ public partial class ProfileManagementViewModel : ObservableObject
|
|||||||
BrowseClientLogoCommand = new AsyncRelayCommand(BrowseClientLogoAsync, () => SelectedProfile != null);
|
BrowseClientLogoCommand = new AsyncRelayCommand(BrowseClientLogoAsync, () => SelectedProfile != null);
|
||||||
ClearClientLogoCommand = new AsyncRelayCommand(ClearClientLogoAsync, () => SelectedProfile != null);
|
ClearClientLogoCommand = new AsyncRelayCommand(ClearClientLogoAsync, () => SelectedProfile != null);
|
||||||
AutoPullClientLogoCommand = new AsyncRelayCommand(AutoPullClientLogoAsync, () => SelectedProfile != null);
|
AutoPullClientLogoCommand = new AsyncRelayCommand(AutoPullClientLogoAsync, () => SelectedProfile != null);
|
||||||
|
RegisterAppCommand = new AsyncRelayCommand(RegisterAppAsync, CanRegisterApp);
|
||||||
|
RemoveAppCommand = new AsyncRelayCommand(RemoveAppAsync, CanRemoveApp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadAsync()
|
public async Task LoadAsync()
|
||||||
@@ -94,6 +113,15 @@ public partial class ProfileManagementViewModel : ObservableObject
|
|||||||
AutoPullClientLogoCommand.NotifyCanExecuteChanged();
|
AutoPullClientLogoCommand.NotifyCanExecuteChanged();
|
||||||
RenameCommand.NotifyCanExecuteChanged();
|
RenameCommand.NotifyCanExecuteChanged();
|
||||||
DeleteCommand.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)
|
private static string? FormatLogoPreview(LogoData? logo)
|
||||||
@@ -256,4 +284,77 @@ public partial class ProfileManagementViewModel : ObservableObject
|
|||||||
_logger.LogWarning(ex, "Auto-pull client logo failed.");
|
_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