diff --git a/SharepointToolbox/App.xaml.cs b/SharepointToolbox/App.xaml.cs index 10ddb98..ff48f72 100644 --- a/SharepointToolbox/App.xaml.cs +++ b/SharepointToolbox/App.xaml.cs @@ -142,6 +142,11 @@ public partial class App : Application services.AddTransient(); services.AddTransient(); + // Versions cleanup + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + // Phase 2: Permissions services.AddTransient(); services.AddTransient(); diff --git a/SharepointToolbox/Core/Models/VersionCleanupOptions.cs b/SharepointToolbox/Core/Models/VersionCleanupOptions.cs new file mode 100644 index 0000000..8e762dc --- /dev/null +++ b/SharepointToolbox/Core/Models/VersionCleanupOptions.cs @@ -0,0 +1,9 @@ +namespace SharepointToolbox.Core.Models; + +public record VersionCleanupOptions( + IReadOnlyList LibraryTitles, + int KeepLast, + bool KeepFirst) +{ + public static VersionCleanupOptions Default => new(Array.Empty(), 5, false); +} diff --git a/SharepointToolbox/Core/Models/VersionCleanupResult.cs b/SharepointToolbox/Core/Models/VersionCleanupResult.cs new file mode 100644 index 0000000..38b9acc --- /dev/null +++ b/SharepointToolbox/Core/Models/VersionCleanupResult.cs @@ -0,0 +1,14 @@ +namespace SharepointToolbox.Core.Models; + +public class VersionCleanupResult +{ + public string SiteUrl { get; init; } = string.Empty; + public string Library { get; init; } = string.Empty; + public string FileServerRelativeUrl { get; init; } = string.Empty; + public string FileName { get; init; } = string.Empty; + public int VersionsBefore { get; init; } + public int VersionsDeleted { get; init; } + public int VersionsRemaining { get; init; } + public long BytesFreed { get; init; } + public string? Error { get; init; } +} diff --git a/SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs b/SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs index 7384bad..a44003c 100644 --- a/SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs +++ b/SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs @@ -45,14 +45,13 @@ public class GraphClientFactory { var pca = await _msalFactory.GetOrCreateAsync(clientId); - // When a tenant is specified we must NOT reuse cached accounts from /common - // (or a different tenant) — they route tokens to the wrong authority. - IAccount? account = null; - if (tenantId is null) - { - var accounts = await pca.GetAccountsAsync(); - account = accounts.FirstOrDefault(); - } + // Always reuse a cached account when one exists — `WithTenantId` on the + // silent/interactive call redirects the authority, and MSAL stores + // refresh tokens per tenant. Skipping the cached account forces an + // interactive prompt on every Graph call (the bug that produced 4–5 + // sign-in windows during app registration). + var accounts = await pca.GetAccountsAsync(); + var account = accounts.FirstOrDefault(); var graphScopes = scopes ?? new[] { "https://graph.microsoft.com/.default" }; @@ -68,7 +67,7 @@ public class GraphClientFactory internal class MsalTokenProvider : IAccessTokenProvider { private readonly IPublicClientApplication _pca; - private readonly IAccount? _account; + private IAccount? _account; private readonly string[] _scopes; private readonly string? _tenantId; @@ -87,19 +86,35 @@ internal class MsalTokenProvider : IAccessTokenProvider Dictionary? additionalAuthenticationContext = null, CancellationToken cancellationToken = default) { - try + // Refresh _account from PCA cache each call — interactive flows on a + // sibling token provider populate the cache, and we want the next + // request on this provider to use that account silently. + if (_account is null) { - var silent = _pca.AcquireTokenSilent(_scopes, _account); - if (_tenantId is not null) silent = silent.WithTenantId(_tenantId); - var result = await silent.ExecuteAsync(cancellationToken); - return result.AccessToken; + var accounts = await _pca.GetAccountsAsync(); + _account = accounts.FirstOrDefault(); } - catch (MsalUiRequiredException) + + if (_account is not null) { - var interactive = _pca.AcquireTokenInteractive(_scopes); - if (_tenantId is not null) interactive = interactive.WithTenantId(_tenantId); - var result = await interactive.ExecuteAsync(cancellationToken); - return result.AccessToken; + try + { + var silent = _pca.AcquireTokenSilent(_scopes, _account); + if (_tenantId is not null) silent = silent.WithTenantId(_tenantId); + var result = await silent.ExecuteAsync(cancellationToken); + return result.AccessToken; + } + catch (MsalUiRequiredException) + { + // fall through to interactive + } } + + var interactive = _pca.AcquireTokenInteractive(_scopes); + if (_tenantId is not null) interactive = interactive.WithTenantId(_tenantId); + var interactiveResult = await interactive.ExecuteAsync(cancellationToken); + // Cache the account so subsequent calls on this provider go silent. + _account = interactiveResult.Account; + return interactiveResult.AccessToken; } } diff --git a/SharepointToolbox/Localization/Strings.fr.resx b/SharepointToolbox/Localization/Strings.fr.resx index e9b942f..2040f58 100644 --- a/SharepointToolbox/Localization/Strings.fr.resx +++ b/SharepointToolbox/Localization/Strings.fr.resx @@ -82,6 +82,103 @@ Doublons + + Versions + + + Nettoyage des versions + + + Bibliothèques + + + Politique de conservation + + + Choisir des bibliothèques… + + + Réinitialiser (toutes les bibliothèques) + + + Supprimer les anciennes versions + + + Conserver les dernières : + + + Conserver aussi la toute première version + + + Demander confirmation avant l'exécution + + + Seules les versions historiques sont supprimées. La version courante publiée est toujours conservée. L'action est irréversible. + + + Toutes les bibliothèques (aucun filtre) + + + {0} bibliothèque(s) sélectionnée(s) + + + Supprimer les versions historiques en gardant les {0} dernières {1} ? +Cette action est irréversible. + + + (plus la première version) + + + « Conserver les dernières » doit être supérieur ou égal à 0. + + + Fichiers nettoyés : + + + Versions supprimées : + + + Octets libérés : + + + Bibliothèque + + + Fichier + + + Avant + + + Supprimées + + + Restantes + + + Libérés + + + Chemin + + + Erreur + + + Sélectionner les bibliothèques + + + Chargement des bibliothèques… + + + {0} bibliothèques chargées. + + + Tout sélectionner + + + Tout désélectionner + Modèles @@ -148,6 +245,18 @@ Supprimer + + Créer un nouveau profil à partir des valeurs ci-dessus. + + + Enregistrer les modifications du profil sélectionné. + + + Supprimer le profil sélectionné. + + + L'enregistrement de l'application peut nécessiter jusqu'à {0} connexions. Continuer ? + Prêt diff --git a/SharepointToolbox/Localization/Strings.resx b/SharepointToolbox/Localization/Strings.resx index b0f2c92..9abcfef 100644 --- a/SharepointToolbox/Localization/Strings.resx +++ b/SharepointToolbox/Localization/Strings.resx @@ -82,6 +82,103 @@ Duplicates + + Versions + + + Version cleanup + + + Libraries + + + Retention policy + + + Select libraries... + + + Reset (all libraries) + + + Delete old versions + + + Keep last: + + + Also keep the very first version + + + Ask for confirmation before running + + + Only historical versions are removed. The current published version is always kept. The action cannot be undone. + + + All libraries (no filter) + + + {0} library/libraries selected + + + Delete historical file versions, keeping the last {0} {1}? +This cannot be undone. + + + (plus the first version) + + + "Keep last" must be 0 or greater. + + + Files trimmed: + + + Versions deleted: + + + Bytes freed: + + + Library + + + File + + + Before + + + Deleted + + + Remaining + + + Freed + + + Path + + + Error + + + Select libraries + + + Loading libraries... + + + {0} libraries loaded. + + + Select all + + + Select none + Templates @@ -148,6 +245,18 @@ Delete + + Create a new profile from the values entered above. + + + Save changes to the selected profile. + + + Delete the selected profile. + + + Registering an app may prompt you to sign in up to {0} times. Continue? + Ready diff --git a/SharepointToolbox/MainWindow.xaml b/SharepointToolbox/MainWindow.xaml index 8766bff..c359e0a 100644 --- a/SharepointToolbox/MainWindow.xaml +++ b/SharepointToolbox/MainWindow.xaml @@ -18,8 +18,6 @@ -