Settings: Database section with connection params, test, and replay button
New « BASE DE DONNÉES (XAMPP / MySQL) » section in Paramètres covering: - Host / Port / User / Password / Database — defaults match a fresh XAMPP install (localhost:3306, root, empty password, "proserve"). Password is DPAPI-encrypted in config.json (same scheme as the license key). - « Tester » button — opens a fresh MySqlConnection and runs SELECT 1. Shows ✓ OK or the connection error inline. - « Appliquer automatiquement les migrations à l'install » checkbox — opt-in for the post-install migration step. Default true. - « 🔁 Rejouer les migrations » button — manually re-runs ApplyMigrationsAsync on the latest installed version. Useful when the post-install run failed (XAMPP was off) or after a dev added a new SQL file. Live status « 3/5 : 0042_add_index.sql » + final « ✓ N applied, M skipped » or « ✗ failure ». - Hint paragraph below explaining the _migrations/ convention. SettingsViewModel - Pulls IDatabaseMigrationService and IInstallationRegistry via DI. - Save() now also persists the DB block, encrypting the password before write. - TestDbAsync temporarily swaps the in-memory config so the service sees the values being typed (without persisting until Save). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,10 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSLauncher.Core.Configuration;
|
||||
using PSLauncher.Core.Downloads;
|
||||
using PSLauncher.Core.Installations;
|
||||
using PSLauncher.Core.Licensing;
|
||||
using PSLauncher.Core.Localization;
|
||||
using PSLauncher.Core.Migrations;
|
||||
using PSLauncher.Models;
|
||||
|
||||
namespace PSLauncher.App.ViewModels;
|
||||
@@ -27,6 +29,8 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
private readonly LocalConfig _config;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly IDownloadStateStore _downloadStore;
|
||||
private readonly IDatabaseMigrationService _migrationService;
|
||||
private readonly IInstallationRegistry _registry;
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<SettingsViewModel> _logger;
|
||||
|
||||
@@ -39,6 +43,18 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
[ObservableProperty] private string _installRoot;
|
||||
[ObservableProperty] private string _language;
|
||||
|
||||
// ----- DB Migration settings -----
|
||||
[ObservableProperty] private string _dbHost;
|
||||
[ObservableProperty] private uint _dbPort;
|
||||
[ObservableProperty] private string _dbUser;
|
||||
[ObservableProperty] private string _dbPassword;
|
||||
[ObservableProperty] private string _dbName;
|
||||
[ObservableProperty] private bool _dbAutoApplyMigrations;
|
||||
[ObservableProperty] private string? _dbConnectionStatus;
|
||||
[ObservableProperty] private bool _isTestingDb;
|
||||
[ObservableProperty] private string? _migrationsReplayStatus;
|
||||
[ObservableProperty] private bool _isReplayingMigrations;
|
||||
|
||||
public IReadOnlyList<LanguageOption> AvailableLanguages { get; } =
|
||||
PSLauncher.Core.Localization.Strings.Available
|
||||
.Select(t => new LanguageOption(t.Code, t.Name))
|
||||
@@ -105,6 +121,8 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
LocalConfig config,
|
||||
ILicenseService licenseService,
|
||||
IDownloadStateStore downloadStore,
|
||||
IDatabaseMigrationService migrationService,
|
||||
IInstallationRegistry registry,
|
||||
HttpClient http,
|
||||
ILogger<SettingsViewModel> logger)
|
||||
{
|
||||
@@ -112,6 +130,8 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
_config = config;
|
||||
_licenseService = licenseService;
|
||||
_downloadStore = downloadStore;
|
||||
_migrationService = migrationService;
|
||||
_registry = registry;
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
|
||||
@@ -119,6 +139,13 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
_installRoot = config.InstallRoot;
|
||||
_language = string.IsNullOrWhiteSpace(config.Language) ? "auto" : config.Language;
|
||||
|
||||
_dbHost = config.Database.Host;
|
||||
_dbPort = config.Database.Port;
|
||||
_dbUser = config.Database.User;
|
||||
_dbPassword = DatabasePasswordProtector.Unprotect(config.Database.EncryptedPassword) ?? string.Empty;
|
||||
_dbName = config.Database.Database;
|
||||
_dbAutoApplyMigrations = config.Database.AutoApplyMigrations;
|
||||
|
||||
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
||||
MachineId = licenseService.GetMachineId();
|
||||
LogsDirectory = App.LogsDirectory;
|
||||
@@ -225,6 +252,17 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
_config.ServerBaseUrl = ServerBaseUrl.TrimEnd('/');
|
||||
_config.InstallRoot = InstallRoot;
|
||||
_config.Language = Language ?? "auto";
|
||||
|
||||
// DB settings
|
||||
_config.Database.Host = DbHost.Trim();
|
||||
_config.Database.Port = DbPort > 0 ? DbPort : 3306;
|
||||
_config.Database.User = DbUser.Trim();
|
||||
_config.Database.EncryptedPassword = string.IsNullOrEmpty(DbPassword)
|
||||
? null
|
||||
: DatabasePasswordProtector.Protect(DbPassword);
|
||||
_config.Database.Database = DbName.Trim();
|
||||
_config.Database.AutoApplyMigrations = DbAutoApplyMigrations;
|
||||
|
||||
_configStore.Save(_config);
|
||||
_logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged);
|
||||
|
||||
@@ -289,4 +327,99 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes) => Strings.FormatSize(bytes);
|
||||
|
||||
// ===================== DB MIGRATIONS =====================
|
||||
|
||||
/// <summary>
|
||||
/// Test la connexion à la DB MySQL avec les paramètres en cours de saisie.
|
||||
/// Persiste temporairement les valeurs sur _config.Database avant le test
|
||||
/// pour que le service les voie via son configProvider, puis remet (et le
|
||||
/// Save() final écrasera quand l'utilisateur cliquera Enregistrer).
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task TestDbAsync()
|
||||
{
|
||||
DbConnectionStatus = Strings.SettingsTest + "…";
|
||||
IsTestingDb = true;
|
||||
// On applique temporairement les valeurs saisies
|
||||
var oldHost = _config.Database.Host;
|
||||
var oldPort = _config.Database.Port;
|
||||
var oldUser = _config.Database.User;
|
||||
var oldPwd = _config.Database.EncryptedPassword;
|
||||
var oldDb = _config.Database.Database;
|
||||
try
|
||||
{
|
||||
_config.Database.Host = DbHost.Trim();
|
||||
_config.Database.Port = DbPort > 0 ? DbPort : 3306;
|
||||
_config.Database.User = DbUser.Trim();
|
||||
_config.Database.EncryptedPassword = string.IsNullOrEmpty(DbPassword)
|
||||
? null
|
||||
: DatabasePasswordProtector.Protect(DbPassword);
|
||||
_config.Database.Database = DbName.Trim();
|
||||
|
||||
var err = await _migrationService.TestConnectionAsync(CancellationToken.None);
|
||||
DbConnectionStatus = err is null ? "✓ OK" : $"✗ {err}";
|
||||
}
|
||||
catch (Exception ex) { DbConnectionStatus = $"✗ {ex.Message}"; }
|
||||
finally
|
||||
{
|
||||
// Restaure : seul Save() doit écrire pour de bon
|
||||
_config.Database.Host = oldHost;
|
||||
_config.Database.Port = oldPort;
|
||||
_config.Database.User = oldUser;
|
||||
_config.Database.EncryptedPassword = oldPwd;
|
||||
_config.Database.Database = oldDb;
|
||||
IsTestingDb = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-joue les migrations bundled dans le _migrations/ de la version actuellement
|
||||
/// installée la plus récente. Utile si l'auto-apply post-install avait échoué (ex.
|
||||
/// XAMPP éteint à ce moment-là), ou pour rejouer manuellement après ajout d'une
|
||||
/// nouvelle migration côté dev.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task ReplayMigrationsAsync()
|
||||
{
|
||||
var installed = _registry.Scan().OrderByDescending(v => v.Version).FirstOrDefault();
|
||||
if (installed is null)
|
||||
{
|
||||
MigrationsReplayStatus = "Aucune version installée — installe d'abord une version avec _migrations/.";
|
||||
return;
|
||||
}
|
||||
IsReplayingMigrations = true;
|
||||
MigrationsReplayStatus = $"Réapplication des migrations pour v{installed.Version}…";
|
||||
try
|
||||
{
|
||||
var progress = new Progress<MigrationProgress>(mp =>
|
||||
{
|
||||
if (mp.Total > 0)
|
||||
MigrationsReplayStatus = $"{mp.Done + 1}/{mp.Total} : {mp.CurrentFilename}";
|
||||
});
|
||||
// Il faut persister les éventuelles modifs DB avant le test (le service lit la config courante)
|
||||
Save();
|
||||
var result = await _migrationService.ApplyMigrationsAsync(
|
||||
installed.FolderPath, progress, CancellationToken.None);
|
||||
if (result.AllSucceeded)
|
||||
{
|
||||
MigrationsReplayStatus = result.AppliedCount > 0
|
||||
? $"✓ {result.AppliedCount} migration(s) appliquée(s), {result.SkippedCount} déjà en place."
|
||||
: $"✓ Tout est à jour ({result.SkippedCount} déjà appliquée(s))";
|
||||
}
|
||||
else
|
||||
{
|
||||
MigrationsReplayStatus = $"✗ Échec : {result.FailureMessage}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Replay migrations failed");
|
||||
MigrationsReplayStatus = $"✗ Erreur : {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsReplayingMigrations = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user