2 Commits
v2.3.1 ... main

Author SHA1 Message Date
Dev
8f30a60d2a Merge branch 'main' of https://git.azuze.fr/kawa/Sharepoint-Toolbox 2026-04-15 14:27:36 +02:00
6e05d26114 Update README.md 2026-04-15 14:27:31 +02:00
6 changed files with 46 additions and 6 deletions

View File

@@ -34,6 +34,19 @@ public partial class App : Application
.Build(); .Build();
host.Start(); host.Start();
// Apply persisted language before any UI is created so bindings resolve to the saved culture.
try
{
var settings = host.Services.GetRequiredService<SettingsService>().GetSettingsAsync().GetAwaiter().GetResult();
if (!string.IsNullOrWhiteSpace(settings.Lang))
Localization.TranslationSource.Instance.CurrentCulture = new System.Globalization.CultureInfo(settings.Lang);
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to apply persisted language at startup");
}
App app = new(); App app = new();
app.InitializeComponent(); app.InitializeComponent();

View File

@@ -124,6 +124,9 @@
<data name="profile.clientid" xml:space="preserve"> <data name="profile.clientid" xml:space="preserve">
<value>ID client</value> <value>ID client</value>
</data> </data>
<data name="profile.clientid.hint" xml:space="preserve">
<value>Optionnel — laissez vide pour enregistrer l'application automatiquement</value>
</data>
<data name="profile.add" xml:space="preserve"> <data name="profile.add" xml:space="preserve">
<value>Ajouter</value> <value>Ajouter</value>
</data> </data>

View File

@@ -124,6 +124,9 @@
<data name="profile.clientid" xml:space="preserve"> <data name="profile.clientid" xml:space="preserve">
<value>Client ID</value> <value>Client ID</value>
</data> </data>
<data name="profile.clientid.hint" xml:space="preserve">
<value>Optional — leave blank to register the app automatically</value>
</data>
<data name="profile.add" xml:space="preserve"> <data name="profile.add" xml:space="preserve">
<value>Add</value> <value>Add</value>
</data> </data>

View File

@@ -24,8 +24,9 @@ public class ProfileService
!Uri.TryCreate(profile.TenantUrl, UriKind.Absolute, out _)) !Uri.TryCreate(profile.TenantUrl, UriKind.Absolute, out _))
throw new ArgumentException("TenantUrl must be a valid absolute URL.", nameof(profile)); throw new ArgumentException("TenantUrl must be a valid absolute URL.", nameof(profile));
if (string.IsNullOrWhiteSpace(profile.ClientId)) // ClientId is optional at creation time: the user can register the app from within
throw new ArgumentException("ClientId must not be empty.", nameof(profile)); // the tool, which will populate ClientId/AppId on the profile afterwards.
profile.ClientId ??= string.Empty;
var existing = (await _repository.LoadAsync()).ToList(); var existing = (await _repository.LoadAsync()).ToList();
existing.Add(profile); existing.Add(profile);

View File

@@ -19,6 +19,11 @@ public partial class ProfileManagementViewModel : ObservableObject
private readonly ILogger<ProfileManagementViewModel> _logger; private readonly ILogger<ProfileManagementViewModel> _logger;
private readonly IAppRegistrationService _appRegistrationService; private readonly IAppRegistrationService _appRegistrationService;
// Well-known public client (Microsoft Graph Command Line Tools) used as a bootstrap
// when a profile has no ClientId yet, so the user can sign in as admin and have the
// app registration created for them.
private const string BootstrapClientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e";
[ObservableProperty] [ObservableProperty]
private TenantProfile? _selectedProfile; private TenantProfile? _selectedProfile;
@@ -137,7 +142,7 @@ public partial class ProfileManagementViewModel : ObservableObject
{ {
if (string.IsNullOrWhiteSpace(NewName)) return false; if (string.IsNullOrWhiteSpace(NewName)) return false;
if (!Uri.TryCreate(NewTenantUrl, UriKind.Absolute, out _)) return false; if (!Uri.TryCreate(NewTenantUrl, UriKind.Absolute, out _)) return false;
if (string.IsNullOrWhiteSpace(NewClientId)) return false; // ClientId is optional — leaving it blank lets the user register the app from within the tool.
return true; return true;
} }
@@ -150,7 +155,7 @@ public partial class ProfileManagementViewModel : ObservableObject
{ {
Name = NewName.Trim(), Name = NewName.Trim(),
TenantUrl = NewTenantUrl.Trim(), TenantUrl = NewTenantUrl.Trim(),
ClientId = NewClientId.Trim() ClientId = NewClientId?.Trim() ?? string.Empty
}; };
await _profileService.AddProfileAsync(profile); await _profileService.AddProfileAsync(profile);
Profiles.Add(profile); Profiles.Add(profile);
@@ -299,7 +304,14 @@ public partial class ProfileManagementViewModel : ObservableObject
RegistrationStatus = TranslationSource.Instance["profile.register.checking"]; RegistrationStatus = TranslationSource.Instance["profile.register.checking"];
try try
{ {
var isAdmin = await _appRegistrationService.IsGlobalAdminAsync(SelectedProfile.ClientId, ct); // Use the profile's own ClientId if it has one; otherwise bootstrap with the
// Microsoft Graph Command Line Tools public client so a first-time profile
// (name + URL only) can still perform the admin check and registration.
var authClientId = string.IsNullOrWhiteSpace(SelectedProfile.ClientId)
? BootstrapClientId
: SelectedProfile.ClientId;
var isAdmin = await _appRegistrationService.IsGlobalAdminAsync(authClientId, ct);
if (!isAdmin) if (!isAdmin)
{ {
ShowFallbackInstructions = true; ShowFallbackInstructions = true;
@@ -308,11 +320,15 @@ public partial class ProfileManagementViewModel : ObservableObject
} }
RegistrationStatus = TranslationSource.Instance["profile.register.registering"]; RegistrationStatus = TranslationSource.Instance["profile.register.registering"];
var result = await _appRegistrationService.RegisterAsync(SelectedProfile.ClientId, SelectedProfile.Name, ct); var result = await _appRegistrationService.RegisterAsync(authClientId, SelectedProfile.Name, ct);
if (result.IsSuccess) if (result.IsSuccess)
{ {
SelectedProfile.AppId = result.AppId; SelectedProfile.AppId = result.AppId;
// If the profile had no ClientId, adopt the freshly registered app's id
// so subsequent sign-ins use the profile's own app registration.
if (string.IsNullOrWhiteSpace(SelectedProfile.ClientId))
SelectedProfile.ClientId = result.AppId!;
await _profileService.UpdateProfileAsync(SelectedProfile); await _profileService.UpdateProfileAsync(SelectedProfile);
RegistrationStatus = TranslationSource.Instance["profile.register.success"]; RegistrationStatus = TranslationSource.Instance["profile.register.success"];
OnPropertyChanged(nameof(HasRegisteredApp)); OnPropertyChanged(nameof(HasRegisteredApp));

View File

@@ -35,6 +35,7 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.name]}" <Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.name]}"
Grid.Row="0" Grid.Column="0" /> Grid.Row="0" Grid.Column="0" />
@@ -48,6 +49,9 @@
Grid.Row="2" Grid.Column="0" /> Grid.Row="2" Grid.Column="0" />
<TextBox Text="{Binding NewClientId, UpdateSourceTrigger=PropertyChanged}" <TextBox Text="{Binding NewClientId, UpdateSourceTrigger=PropertyChanged}"
Grid.Row="2" Grid.Column="1" Margin="0,2" /> Grid.Row="2" Grid.Column="1" Margin="0,2" />
<TextBlock Grid.Row="3" Grid.Column="1" Margin="0,2,0,0"
FontSize="11" FontStyle="Italic" Foreground="#666666" TextWrapping="Wrap"
Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.clientid.hint]}" />
</Grid> </Grid>
<!-- Client Logo --> <!-- Client Logo -->