using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; using OTSSignsOrchestrator.Core.Models.Entities; using OTSSignsOrchestrator.Core.Services; using OTSSignsOrchestrator.Desktop.Models; using OTSSignsOrchestrator.Desktop.Services; namespace OTSSignsOrchestrator.Desktop.ViewModels; /// /// ViewModel for the instance details modal. /// Shows admin credentials, DB credentials, and OAuth2 app details /// with options to rotate passwords. /// public partial class InstanceDetailsViewModel : ObservableObject { private readonly IServiceProvider _services; // ── Instance metadata ───────────────────────────────────────────────────── [ObservableProperty] private string _stackName = string.Empty; [ObservableProperty] private string _customerAbbrev = string.Empty; [ObservableProperty] private string _hostLabel = string.Empty; [ObservableProperty] private string _instanceUrl = string.Empty; // ── OTS admin credentials ───────────────────────────────────────────────── [ObservableProperty] private string _adminUsername = string.Empty; [ObservableProperty] private string _adminPassword = string.Empty; [ObservableProperty] private bool _adminPasswordVisible = false; [ObservableProperty] private string _adminPasswordDisplay = "••••••••"; // ── Database credentials ────────────────────────────────────────────────── [ObservableProperty] private string _dbUsername = string.Empty; [ObservableProperty] private string _dbPassword = string.Empty; [ObservableProperty] private bool _dbPasswordVisible = false; [ObservableProperty] private string _dbPasswordDisplay = "••••••••"; // ── OAuth2 application ──────────────────────────────────────────────────── [ObservableProperty] private string _oAuthClientId = string.Empty; [ObservableProperty] private string _oAuthClientSecret = string.Empty; [ObservableProperty] private bool _oAuthSecretVisible = false; [ObservableProperty] private string _oAuthSecretDisplay = "••••••••"; // ── Status ──────────────────────────────────────────────────────────────── [ObservableProperty] private string _statusMessage = string.Empty; [ObservableProperty] private bool _isBusy; public InstanceDetailsViewModel(IServiceProvider services) { _services = services; } // ───────────────────────────────────────────────────────────────────────── // Load // ───────────────────────────────────────────────────────────────────────── /// Populates the ViewModel from a live . public async Task LoadAsync(LiveStackItem instance) { StackName = instance.StackName; CustomerAbbrev = instance.CustomerAbbrev; HostLabel = instance.HostLabel; IsBusy = true; StatusMessage = "Loading credentials..."; try { using var scope = _services.CreateScope(); var settings = scope.ServiceProvider.GetRequiredService(); var postInit = scope.ServiceProvider.GetRequiredService(); // Derive the instance URL from the CMS server name template var serverTemplate = await settings.GetAsync( SettingsService.DefaultCmsServerNameTemplate, "{abbrev}.ots-signs.com"); var serverName = serverTemplate.Replace("{abbrev}", instance.CustomerAbbrev); InstanceUrl = $"https://{serverName}"; // ── Admin credentials ───────────────────────────────────────── var creds = await postInit.GetCredentialsAsync(instance.CustomerAbbrev); AdminUsername = creds.AdminUsername; SetAdminPassword(creds.AdminPassword ?? string.Empty); OAuthClientId = creds.OAuthClientId ?? string.Empty; SetOAuthSecret(creds.OAuthClientSecret ?? string.Empty); // ── DB credentials ──────────────────────────────────────────── var mySqlUserTemplate = await settings.GetAsync( SettingsService.DefaultMySqlUserTemplate, "{abbrev}_cms_user"); DbUsername = mySqlUserTemplate.Replace("{abbrev}", instance.CustomerAbbrev); var dbPw = await settings.GetAsync(SettingsService.InstanceMySqlPassword(instance.CustomerAbbrev)); SetDbPassword(dbPw ?? string.Empty); StatusMessage = creds.HasAdminPassword ? "Credentials loaded." : "Credentials not yet available — post-install setup may still be running."; } catch (Exception ex) { StatusMessage = $"Error loading credentials: {ex.Message}"; } finally { IsBusy = false; } } // ───────────────────────────────────────────────────────────────────────── // Visibility toggles // ───────────────────────────────────────────────────────────────────────── [RelayCommand] private void ToggleAdminPasswordVisibility() { AdminPasswordVisible = !AdminPasswordVisible; AdminPasswordDisplay = AdminPasswordVisible ? AdminPassword : (AdminPassword.Length > 0 ? "••••••••" : "(not set)"); } [RelayCommand] private void ToggleDbPasswordVisibility() { DbPasswordVisible = !DbPasswordVisible; DbPasswordDisplay = DbPasswordVisible ? DbPassword : (DbPassword.Length > 0 ? "••••••••" : "(not set)"); } [RelayCommand] private void ToggleOAuthSecretVisibility() { OAuthSecretVisible = !OAuthSecretVisible; OAuthSecretDisplay = OAuthSecretVisible ? OAuthClientSecret : (OAuthClientSecret.Length > 0 ? "••••••••" : "(not set)"); } // ───────────────────────────────────────────────────────────────────────── // Clipboard // ───────────────────────────────────────────────────────────────────────── [RelayCommand] private async Task CopyAdminPasswordAsync() => await CopyToClipboardAsync(AdminPassword, "Admin password"); [RelayCommand] private async Task CopyDbPasswordAsync() => await CopyToClipboardAsync(DbPassword, "DB password"); [RelayCommand] private async Task CopyOAuthClientIdAsync() => await CopyToClipboardAsync(OAuthClientId, "OAuth client ID"); [RelayCommand] private async Task CopyOAuthSecretAsync() => await CopyToClipboardAsync(OAuthClientSecret, "OAuth client secret"); // ───────────────────────────────────────────────────────────────────────── // Rotation // ───────────────────────────────────────────────────────────────────────── [RelayCommand] private async Task RotateAdminPasswordAsync() { if (string.IsNullOrWhiteSpace(CustomerAbbrev)) return; IsBusy = true; StatusMessage = "Rotating OTS admin password..."; try { var postInit = _services.GetRequiredService(); var newPassword = await postInit.RotateAdminPasswordAsync(CustomerAbbrev, InstanceUrl); SetAdminPassword(newPassword); StatusMessage = "Admin password rotated successfully."; } catch (Exception ex) { StatusMessage = $"Error rotating admin password: {ex.Message}"; } finally { IsBusy = false; } } [RelayCommand] private async Task RotateDbPasswordAsync() { if (string.IsNullOrWhiteSpace(CustomerAbbrev)) return; IsBusy = true; StatusMessage = $"Rotating MySQL password for {StackName}..."; try { var dockerCli = _services.GetRequiredService(); var dockerSecrets = _services.GetRequiredService(); // We need the Host — retrieve from the HostLabel lookup using var scope = _services.CreateScope(); var instanceSvc = scope.ServiceProvider.GetRequiredService(); // Get the host from the loaded stack — caller must have set the SSH host before var (ok, msg) = await instanceSvc.RotateMySqlPasswordAsync(StackName); if (ok) { // Reload DB password var settings = scope.ServiceProvider.GetRequiredService(); var dbPw = await settings.GetAsync(SettingsService.InstanceMySqlPassword(CustomerAbbrev)); SetDbPassword(dbPw ?? string.Empty); StatusMessage = $"DB password rotated: {msg}"; } else { StatusMessage = $"DB rotation failed: {msg}"; } } catch (Exception ex) { StatusMessage = $"Error rotating DB password: {ex.Message}"; } finally { IsBusy = false; } } // ───────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────── private void SetAdminPassword(string value) { AdminPassword = value; AdminPasswordVisible = false; AdminPasswordDisplay = value.Length > 0 ? "••••••••" : "(not set)"; } private void SetDbPassword(string value) { DbPassword = value; DbPasswordVisible = false; DbPasswordDisplay = value.Length > 0 ? "••••••••" : "(not set)"; } private void SetOAuthSecret(string value) { OAuthClientSecret = value; OAuthSecretVisible = false; OAuthSecretDisplay = value.Length > 0 ? "••••••••" : "(not set)"; } private static async Task CopyToClipboardAsync(string text, string label) { if (string.IsNullOrEmpty(text)) return; var topLevel = Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime dt ? dt.MainWindow : null; var clipboard = topLevel is not null ? Avalonia.Controls.TopLevel.GetTopLevel(topLevel)?.Clipboard : null; if (clipboard is not null) await clipboard.SetTextAsync(text); } }