585 lines
30 KiB
C#
585 lines
30 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Options;
|
|
using OTSSignsOrchestrator.Core.Configuration;
|
|
using OTSSignsOrchestrator.Core.Models.DTOs;
|
|
using OTSSignsOrchestrator.Core.Services;
|
|
|
|
namespace OTSSignsOrchestrator.Desktop.ViewModels;
|
|
|
|
/// <summary>
|
|
/// ViewModel for the Settings page — manages Git, MySQL, SMTP, Pangolin, NFS,
|
|
/// and Instance Defaults configuration, persisted via SettingsService.
|
|
/// </summary>
|
|
public partial class SettingsViewModel : ObservableObject
|
|
{
|
|
private readonly IServiceProvider _services;
|
|
|
|
[ObservableProperty] private string _statusMessage = string.Empty;
|
|
[ObservableProperty] private bool _isBusy;
|
|
|
|
// ── Git ──────────────────────────────────────────────────────────────────
|
|
[ObservableProperty] private string _gitRepoUrl = string.Empty;
|
|
[ObservableProperty] private string _gitRepoPat = string.Empty;
|
|
|
|
// ── MySQL Admin ─────────────────────────────────────────────────────────
|
|
[ObservableProperty] private string _mySqlHost = string.Empty;
|
|
[ObservableProperty] private string _mySqlPort = "3306";
|
|
[ObservableProperty] private string _mySqlAdminUser = string.Empty;
|
|
[ObservableProperty] private string _mySqlAdminPassword = string.Empty;
|
|
|
|
// ── SMTP ────────────────────────────────────────────────────────────────
|
|
[ObservableProperty] private string _smtpServer = string.Empty;
|
|
[ObservableProperty] private string _smtpUsername = string.Empty;
|
|
[ObservableProperty] private string _smtpPassword = string.Empty;
|
|
[ObservableProperty] private bool _smtpUseTls = true;
|
|
[ObservableProperty] private bool _smtpUseStartTls = true;
|
|
[ObservableProperty] private string _smtpRewriteDomain = string.Empty;
|
|
[ObservableProperty] private string _smtpHostname = string.Empty;
|
|
[ObservableProperty] private string _smtpFromLineOverride = "NO";
|
|
|
|
// ── Pangolin ────────────────────────────────────────────────────────────
|
|
[ObservableProperty] private string _pangolinEndpoint = "https://app.pangolin.net";
|
|
|
|
// ── NFS ─────────────────────────────────────────────────────────────────
|
|
[ObservableProperty] private string _nfsServer = string.Empty;
|
|
[ObservableProperty] private string _nfsExport = string.Empty;
|
|
[ObservableProperty] private string _nfsExportFolder = string.Empty;
|
|
[ObservableProperty] private string _nfsOptions = string.Empty;
|
|
|
|
// ── Instance Defaults ───────────────────────────────────────────────────
|
|
[ObservableProperty] private string _defaultCmsImage = "ghcr.io/xibosignage/xibo-cms:release-4.2.3";
|
|
[ObservableProperty] private string _defaultNewtImage = "fosrl/newt";
|
|
[ObservableProperty] private string _defaultMemcachedImage = "memcached:alpine";
|
|
[ObservableProperty] private string _defaultQuickChartImage = "ianw/quickchart";
|
|
[ObservableProperty] private string _defaultCmsServerNameTemplate = "app.ots-signs.com";
|
|
[ObservableProperty] private string _defaultThemeHostPath = "/cms/{abbrev}-cms-theme-custom";
|
|
[ObservableProperty] private string _defaultMySqlDbTemplate = "{abbrev}_cms_db";
|
|
[ObservableProperty] private string _defaultMySqlUserTemplate = "{abbrev}_cms_user";
|
|
[ObservableProperty] private string _defaultPhpPostMaxSize = "10G";
|
|
[ObservableProperty] private string _defaultPhpUploadMaxFilesize = "10G";
|
|
[ObservableProperty] private string _defaultPhpMaxExecutionTime = "600";
|
|
|
|
// ── Bitwarden Secrets Manager ─────────────────────────────────
|
|
[ObservableProperty] private string _bitwardenIdentityUrl = "https://identity.bitwarden.com";
|
|
[ObservableProperty] private string _bitwardenApiUrl = "https://api.bitwarden.com";
|
|
[ObservableProperty] private string _bitwardenAccessToken = string.Empty;
|
|
[ObservableProperty] private string _bitwardenOrganizationId = string.Empty;
|
|
[ObservableProperty] private string _bitwardenProjectId = string.Empty;
|
|
[ObservableProperty] private string _bitwardenInstanceProjectId = string.Empty;
|
|
|
|
// ── Authentik (SAML IdP) ────────────────────────────────────────
|
|
[ObservableProperty] private string _authentikUrl = string.Empty;
|
|
[ObservableProperty] private string _authentikApiKey = string.Empty;
|
|
[ObservableProperty] private string _authentikAuthorizationFlowSlug = string.Empty;
|
|
[ObservableProperty] private string _authentikInvalidationFlowSlug = string.Empty;
|
|
[ObservableProperty] private string _authentikSigningKeypairId = string.Empty;
|
|
[ObservableProperty] private string _authentikStatusMessage = string.Empty;
|
|
[ObservableProperty] private bool _isAuthentikBusy;
|
|
|
|
// Dropdown collections for Authentik flows / keypairs
|
|
public ObservableCollection<AuthentikFlowItem> AuthentikAuthorizationFlows { get; } = new();
|
|
public ObservableCollection<AuthentikFlowItem> AuthentikInvalidationFlows { get; } = new();
|
|
public ObservableCollection<AuthentikKeypairItem> AuthentikKeypairs { get; } = new();
|
|
|
|
[ObservableProperty] private AuthentikFlowItem? _selectedAuthorizationFlow;
|
|
[ObservableProperty] private AuthentikFlowItem? _selectedInvalidationFlow;
|
|
[ObservableProperty] private AuthentikKeypairItem? _selectedSigningKeypair;
|
|
|
|
// ── Xibo Bootstrap OAuth2 ─────────────────────────────────────
|
|
[ObservableProperty] private string _xiboBootstrapClientId = string.Empty;
|
|
[ObservableProperty] private string _xiboBootstrapClientSecret = string.Empty;
|
|
|
|
public SettingsViewModel(IServiceProvider services)
|
|
{
|
|
_services = services;
|
|
_ = LoadAsync();
|
|
}
|
|
|
|
/// <summary>Whether Bitwarden is configured and reachable.</summary>
|
|
[ObservableProperty] private bool _isBitwardenConfigured;
|
|
|
|
[RelayCommand]
|
|
private Task LoadAsync() => LoadCoreAsync(skipBitwarden: false);
|
|
|
|
private async Task LoadCoreAsync(bool skipBitwarden)
|
|
{
|
|
IsBusy = true;
|
|
try
|
|
{
|
|
if (!skipBitwarden)
|
|
{
|
|
// ── Load Bitwarden bootstrap config (IOptionsMonitor picks up appsettings.json changes) ──
|
|
var bwOptions = _services.GetRequiredService<IOptionsMonitor<BitwardenOptions>>().CurrentValue;
|
|
BitwardenIdentityUrl = bwOptions.IdentityUrl;
|
|
BitwardenApiUrl = bwOptions.ApiUrl;
|
|
BitwardenAccessToken = bwOptions.AccessToken;
|
|
BitwardenOrganizationId = bwOptions.OrganizationId;
|
|
BitwardenProjectId = bwOptions.ProjectId;
|
|
BitwardenInstanceProjectId = bwOptions.InstanceProjectId;
|
|
}
|
|
|
|
IsBitwardenConfigured = !string.IsNullOrWhiteSpace(BitwardenAccessToken)
|
|
&& !string.IsNullOrWhiteSpace(BitwardenOrganizationId);
|
|
|
|
if (!IsBitwardenConfigured)
|
|
{
|
|
StatusMessage = "Bitwarden is not configured. Fill in the Bitwarden section and save to get started.";
|
|
return;
|
|
}
|
|
|
|
// ── Load all other settings from Bitwarden ──
|
|
using var scope = _services.CreateScope();
|
|
var svc = scope.ServiceProvider.GetRequiredService<SettingsService>();
|
|
svc.InvalidateCache();
|
|
|
|
// Git
|
|
GitRepoUrl = await svc.GetAsync(SettingsService.GitRepoUrl, string.Empty);
|
|
GitRepoPat = await svc.GetAsync(SettingsService.GitRepoPat, string.Empty);
|
|
|
|
// MySQL
|
|
MySqlHost = await svc.GetAsync(SettingsService.MySqlHost, string.Empty);
|
|
MySqlPort = await svc.GetAsync(SettingsService.MySqlPort, "3306");
|
|
MySqlAdminUser = await svc.GetAsync(SettingsService.MySqlAdminUser, string.Empty);
|
|
MySqlAdminPassword = await svc.GetAsync(SettingsService.MySqlAdminPassword, string.Empty);
|
|
|
|
// SMTP
|
|
SmtpServer = await svc.GetAsync(SettingsService.SmtpServer, string.Empty);
|
|
SmtpUsername = await svc.GetAsync(SettingsService.SmtpUsername, string.Empty);
|
|
SmtpPassword = await svc.GetAsync(SettingsService.SmtpPassword, string.Empty);
|
|
SmtpUseTls = (await svc.GetAsync(SettingsService.SmtpUseTls, "YES")) == "YES";
|
|
SmtpUseStartTls = (await svc.GetAsync(SettingsService.SmtpUseStartTls, "YES")) == "YES";
|
|
SmtpRewriteDomain = await svc.GetAsync(SettingsService.SmtpRewriteDomain, string.Empty);
|
|
SmtpHostname = await svc.GetAsync(SettingsService.SmtpHostname, string.Empty);
|
|
SmtpFromLineOverride = await svc.GetAsync(SettingsService.SmtpFromLineOverride, "NO");
|
|
|
|
// Pangolin
|
|
PangolinEndpoint = await svc.GetAsync(SettingsService.PangolinEndpoint, "https://app.pangolin.net");
|
|
|
|
// NFS
|
|
NfsServer = await svc.GetAsync(SettingsService.NfsServer, string.Empty);
|
|
NfsExport = await svc.GetAsync(SettingsService.NfsExport, string.Empty);
|
|
NfsExportFolder = await svc.GetAsync(SettingsService.NfsExportFolder, string.Empty);
|
|
NfsOptions = await svc.GetAsync(SettingsService.NfsOptions, string.Empty);
|
|
|
|
// Instance Defaults
|
|
DefaultCmsImage = await svc.GetAsync(SettingsService.DefaultCmsImage, "ghcr.io/xibosignage/xibo-cms:release-4.2.3");
|
|
DefaultNewtImage = await svc.GetAsync(SettingsService.DefaultNewtImage, "fosrl/newt");
|
|
DefaultMemcachedImage = await svc.GetAsync(SettingsService.DefaultMemcachedImage, "memcached:alpine");
|
|
DefaultQuickChartImage = await svc.GetAsync(SettingsService.DefaultQuickChartImage, "ianw/quickchart");
|
|
DefaultCmsServerNameTemplate = await svc.GetAsync(SettingsService.DefaultCmsServerNameTemplate, "app.ots-signs.com");
|
|
DefaultThemeHostPath = await svc.GetAsync(SettingsService.DefaultThemeHostPath, "/cms/{abbrev}-cms-theme-custom");
|
|
DefaultMySqlDbTemplate = await svc.GetAsync(SettingsService.DefaultMySqlDbTemplate, "{abbrev}_cms_db");
|
|
DefaultMySqlUserTemplate = await svc.GetAsync(SettingsService.DefaultMySqlUserTemplate, "{abbrev}_cms_user");
|
|
DefaultPhpPostMaxSize = await svc.GetAsync(SettingsService.DefaultPhpPostMaxSize, "10G");
|
|
DefaultPhpUploadMaxFilesize = await svc.GetAsync(SettingsService.DefaultPhpUploadMaxFilesize, "10G");
|
|
DefaultPhpMaxExecutionTime = await svc.GetAsync(SettingsService.DefaultPhpMaxExecutionTime, "600");
|
|
|
|
// Authentik
|
|
AuthentikUrl = await svc.GetAsync(SettingsService.AuthentikUrl, string.Empty);
|
|
AuthentikApiKey = await svc.GetAsync(SettingsService.AuthentikApiKey, string.Empty);
|
|
AuthentikAuthorizationFlowSlug = await svc.GetAsync(SettingsService.AuthentikAuthorizationFlowSlug, string.Empty);
|
|
AuthentikInvalidationFlowSlug = await svc.GetAsync(SettingsService.AuthentikInvalidationFlowSlug, string.Empty);
|
|
AuthentikSigningKeypairId = await svc.GetAsync(SettingsService.AuthentikSigningKeypairId, string.Empty);
|
|
|
|
// If Authentik URL + key are configured, try loading dropdowns
|
|
if (!string.IsNullOrWhiteSpace(AuthentikUrl) && !string.IsNullOrWhiteSpace(AuthentikApiKey))
|
|
await FetchAuthentikDropdownsInternalAsync();
|
|
|
|
// Xibo Bootstrap
|
|
XiboBootstrapClientId = await svc.GetAsync(SettingsService.XiboBootstrapClientId, string.Empty);
|
|
XiboBootstrapClientSecret = await svc.GetAsync(SettingsService.XiboBootstrapClientSecret, string.Empty);
|
|
|
|
StatusMessage = "Settings loaded from Bitwarden.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"Error loading settings: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task SaveBitwardenLocalAsync()
|
|
{
|
|
IsBusy = true;
|
|
try
|
|
{
|
|
await SaveBitwardenConfigToFileAsync();
|
|
|
|
IsBitwardenConfigured = !string.IsNullOrWhiteSpace(BitwardenAccessToken)
|
|
&& !string.IsNullOrWhiteSpace(BitwardenOrganizationId);
|
|
|
|
StatusMessage = IsBitwardenConfigured
|
|
? "Bitwarden config saved to appsettings.json."
|
|
: "Bitwarden config saved. Fill in Access Token and Org ID to enable all settings.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"Error saving Bitwarden config: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task PullFromBitwardenAsync()
|
|
{
|
|
await LoadCoreAsync(skipBitwarden: true);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task PushToBitwardenAsync()
|
|
{
|
|
IsBusy = true;
|
|
try
|
|
{
|
|
if (!IsBitwardenConfigured)
|
|
{
|
|
StatusMessage = "Bitwarden is not configured. Save Bitwarden config first.";
|
|
return;
|
|
}
|
|
|
|
using var scope = _services.CreateScope();
|
|
var svc = scope.ServiceProvider.GetRequiredService<SettingsService>();
|
|
svc.InvalidateCache();
|
|
|
|
var settings = new List<(string Key, string? Value, string Category, bool IsSensitive)>
|
|
{
|
|
// Git
|
|
(SettingsService.GitRepoUrl, NullIfEmpty(GitRepoUrl), SettingsService.CatGit, false),
|
|
(SettingsService.GitRepoPat, NullIfEmpty(GitRepoPat), SettingsService.CatGit, true),
|
|
|
|
// MySQL
|
|
(SettingsService.MySqlHost, NullIfEmpty(MySqlHost), SettingsService.CatMySql, false),
|
|
(SettingsService.MySqlPort, MySqlPort, SettingsService.CatMySql, false),
|
|
(SettingsService.MySqlAdminUser, NullIfEmpty(MySqlAdminUser), SettingsService.CatMySql, false),
|
|
(SettingsService.MySqlAdminPassword, NullIfEmpty(MySqlAdminPassword), SettingsService.CatMySql, true),
|
|
|
|
// SMTP
|
|
(SettingsService.SmtpServer, NullIfEmpty(SmtpServer), SettingsService.CatSmtp, false),
|
|
(SettingsService.SmtpUsername, NullIfEmpty(SmtpUsername), SettingsService.CatSmtp, false),
|
|
(SettingsService.SmtpPassword, NullIfEmpty(SmtpPassword), SettingsService.CatSmtp, true),
|
|
(SettingsService.SmtpUseTls, SmtpUseTls ? "YES" : "NO", SettingsService.CatSmtp, false),
|
|
(SettingsService.SmtpUseStartTls, SmtpUseStartTls ? "YES" : "NO", SettingsService.CatSmtp, false),
|
|
(SettingsService.SmtpRewriteDomain, NullIfEmpty(SmtpRewriteDomain), SettingsService.CatSmtp, false),
|
|
(SettingsService.SmtpHostname, NullIfEmpty(SmtpHostname), SettingsService.CatSmtp, false),
|
|
(SettingsService.SmtpFromLineOverride, SmtpFromLineOverride, SettingsService.CatSmtp, false),
|
|
|
|
// Pangolin
|
|
(SettingsService.PangolinEndpoint, NullIfEmpty(PangolinEndpoint), SettingsService.CatPangolin, false),
|
|
|
|
// NFS
|
|
(SettingsService.NfsServer, NullIfEmpty(NfsServer), SettingsService.CatNfs, false),
|
|
(SettingsService.NfsExport, NullIfEmpty(NfsExport), SettingsService.CatNfs, false),
|
|
(SettingsService.NfsExportFolder, NullIfEmpty(NfsExportFolder), SettingsService.CatNfs, false),
|
|
(SettingsService.NfsOptions, NullIfEmpty(NfsOptions), SettingsService.CatNfs, false),
|
|
|
|
// Instance Defaults
|
|
(SettingsService.DefaultCmsImage, DefaultCmsImage, SettingsService.CatDefaults, false),
|
|
(SettingsService.DefaultNewtImage, DefaultNewtImage, SettingsService.CatDefaults, false),
|
|
(SettingsService.DefaultMemcachedImage, DefaultMemcachedImage, SettingsService.CatDefaults, false),
|
|
(SettingsService.DefaultQuickChartImage, DefaultQuickChartImage, SettingsService.CatDefaults, false),
|
|
(SettingsService.DefaultCmsServerNameTemplate, DefaultCmsServerNameTemplate, SettingsService.CatDefaults, false),
|
|
(SettingsService.DefaultThemeHostPath, DefaultThemeHostPath, SettingsService.CatDefaults, false),
|
|
(SettingsService.DefaultMySqlDbTemplate, DefaultMySqlDbTemplate, SettingsService.CatDefaults, false),
|
|
(SettingsService.DefaultMySqlUserTemplate, DefaultMySqlUserTemplate, SettingsService.CatDefaults, false),
|
|
(SettingsService.DefaultPhpPostMaxSize, DefaultPhpPostMaxSize, SettingsService.CatDefaults, false),
|
|
(SettingsService.DefaultPhpUploadMaxFilesize, DefaultPhpUploadMaxFilesize, SettingsService.CatDefaults, false),
|
|
(SettingsService.DefaultPhpMaxExecutionTime, DefaultPhpMaxExecutionTime, SettingsService.CatDefaults, false),
|
|
|
|
// Authentik
|
|
(SettingsService.AuthentikUrl, NullIfEmpty(AuthentikUrl), SettingsService.CatAuthentik, false),
|
|
(SettingsService.AuthentikApiKey, NullIfEmpty(AuthentikApiKey), SettingsService.CatAuthentik, true),
|
|
(SettingsService.AuthentikAuthorizationFlowSlug, NullIfEmpty(SelectedAuthorizationFlow?.Slug ?? AuthentikAuthorizationFlowSlug), SettingsService.CatAuthentik, false),
|
|
(SettingsService.AuthentikInvalidationFlowSlug, NullIfEmpty(SelectedInvalidationFlow?.Slug ?? AuthentikInvalidationFlowSlug), SettingsService.CatAuthentik, false),
|
|
(SettingsService.AuthentikSigningKeypairId, NullIfEmpty(SelectedSigningKeypair?.Pk ?? AuthentikSigningKeypairId), SettingsService.CatAuthentik, false),
|
|
|
|
// Xibo Bootstrap
|
|
(SettingsService.XiboBootstrapClientId, NullIfEmpty(XiboBootstrapClientId), SettingsService.CatXibo, false),
|
|
(SettingsService.XiboBootstrapClientSecret, NullIfEmpty(XiboBootstrapClientSecret), SettingsService.CatXibo, true),
|
|
};
|
|
|
|
await svc.SaveManyAsync(settings);
|
|
StatusMessage = "Settings pushed to Bitwarden.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"Error saving settings: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task TestBitwardenConnectionAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(BitwardenAccessToken) || string.IsNullOrWhiteSpace(BitwardenOrganizationId))
|
|
{
|
|
StatusMessage = "Bitwarden Access Token and Organization ID are required.";
|
|
return;
|
|
}
|
|
|
|
IsBusy = true;
|
|
StatusMessage = "Saving Bitwarden config and testing connection...";
|
|
try
|
|
{
|
|
// Save to appsettings.json first so the service picks up fresh values
|
|
await SaveBitwardenConfigToFileAsync();
|
|
|
|
using var scope = _services.CreateScope();
|
|
var bws = scope.ServiceProvider.GetRequiredService<IBitwardenSecretService>();
|
|
var secrets = await bws.ListSecretsAsync();
|
|
IsBitwardenConfigured = true;
|
|
StatusMessage = $"Bitwarden connected. Found {secrets.Count} secret(s) in project.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
IsBitwardenConfigured = false;
|
|
StatusMessage = $"Bitwarden connection failed: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Authentik: save, test, fetch dropdowns
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
[RelayCommand]
|
|
private async Task SaveAndTestAuthentikAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(AuthentikUrl) || string.IsNullOrWhiteSpace(AuthentikApiKey))
|
|
{
|
|
AuthentikStatusMessage = "Authentik URL and API Token are required.";
|
|
return;
|
|
}
|
|
|
|
IsAuthentikBusy = true;
|
|
AuthentikStatusMessage = "Saving Authentik settings and testing connection...";
|
|
try
|
|
{
|
|
// Persist URL + API key first so subsequent calls work
|
|
using var scope = _services.CreateScope();
|
|
var svc = scope.ServiceProvider.GetRequiredService<SettingsService>();
|
|
await svc.SetAsync(SettingsService.AuthentikUrl, AuthentikUrl.Trim(), SettingsService.CatAuthentik);
|
|
await svc.SetAsync(SettingsService.AuthentikApiKey, AuthentikApiKey.Trim(), SettingsService.CatAuthentik, isSensitive: true);
|
|
|
|
var authentik = scope.ServiceProvider.GetRequiredService<IAuthentikService>();
|
|
var (ok, msg) = await authentik.TestConnectionAsync(AuthentikUrl.Trim(), AuthentikApiKey.Trim());
|
|
|
|
if (!ok)
|
|
{
|
|
AuthentikStatusMessage = $"Connection failed: {msg}";
|
|
return;
|
|
}
|
|
|
|
AuthentikStatusMessage = "Connected — loading flows and keypairs...";
|
|
|
|
// Now fetch dropdowns
|
|
await FetchAuthentikDropdownsInternalAsync(AuthentikUrl.Trim(), AuthentikApiKey.Trim());
|
|
|
|
// Save selected flow/keypair values
|
|
var authSlug = SelectedAuthorizationFlow?.Slug ?? AuthentikAuthorizationFlowSlug;
|
|
var invalSlug = SelectedInvalidationFlow?.Slug ?? AuthentikInvalidationFlowSlug;
|
|
var kpId = SelectedSigningKeypair?.Pk ?? AuthentikSigningKeypairId;
|
|
|
|
await svc.SetAsync(SettingsService.AuthentikAuthorizationFlowSlug, authSlug, SettingsService.CatAuthentik);
|
|
await svc.SetAsync(SettingsService.AuthentikInvalidationFlowSlug, invalSlug, SettingsService.CatAuthentik);
|
|
await svc.SetAsync(SettingsService.AuthentikSigningKeypairId, kpId, SettingsService.CatAuthentik);
|
|
|
|
AuthentikStatusMessage = $"Authentik connected. {AuthentikAuthorizationFlows.Count} flows, {AuthentikKeypairs.Count} keypair(s) loaded.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AuthentikStatusMessage = $"Error: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
IsAuthentikBusy = false;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task FetchAuthentikDropdownsAsync()
|
|
{
|
|
IsAuthentikBusy = true;
|
|
AuthentikStatusMessage = "Fetching flows and keypairs from Authentik...";
|
|
try
|
|
{
|
|
await FetchAuthentikDropdownsInternalAsync();
|
|
AuthentikStatusMessage = $"Loaded {AuthentikAuthorizationFlows.Count} flows, {AuthentikKeypairs.Count} keypair(s).";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AuthentikStatusMessage = $"Error fetching data: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
IsAuthentikBusy = false;
|
|
}
|
|
}
|
|
|
|
private async Task FetchAuthentikDropdownsInternalAsync(
|
|
string? overrideUrl = null, string? overrideApiKey = null)
|
|
{
|
|
using var scope = _services.CreateScope();
|
|
var authentik = scope.ServiceProvider.GetRequiredService<IAuthentikService>();
|
|
|
|
var flows = await authentik.ListFlowsAsync(overrideUrl, overrideApiKey);
|
|
var keypairs = await authentik.ListKeypairsAsync(overrideUrl, overrideApiKey);
|
|
|
|
// Populate authorization flows (designation = "authorization")
|
|
AuthentikAuthorizationFlows.Clear();
|
|
foreach (var f in flows.Where(f => f.Designation == "authorization"))
|
|
AuthentikAuthorizationFlows.Add(f);
|
|
|
|
// Populate invalidation flows (designation = "invalidation")
|
|
AuthentikInvalidationFlows.Clear();
|
|
foreach (var f in flows.Where(f => f.Designation == "invalidation"))
|
|
AuthentikInvalidationFlows.Add(f);
|
|
|
|
// Populate keypairs
|
|
AuthentikKeypairs.Clear();
|
|
// Add a "None" option
|
|
AuthentikKeypairs.Add(new AuthentikKeypairItem { Pk = "", Name = "(none)" });
|
|
foreach (var k in keypairs)
|
|
AuthentikKeypairs.Add(k);
|
|
|
|
// Select items matching saved slugs
|
|
SelectedAuthorizationFlow = AuthentikAuthorizationFlows
|
|
.FirstOrDefault(f => string.Equals(f.Slug, AuthentikAuthorizationFlowSlug, StringComparison.OrdinalIgnoreCase))
|
|
?? AuthentikAuthorizationFlows.FirstOrDefault(f => f.Slug == "default-provider-authorization-implicit-consent")
|
|
?? AuthentikAuthorizationFlows.FirstOrDefault();
|
|
|
|
SelectedInvalidationFlow = AuthentikInvalidationFlows
|
|
.FirstOrDefault(f => string.Equals(f.Slug, AuthentikInvalidationFlowSlug, StringComparison.OrdinalIgnoreCase))
|
|
?? AuthentikInvalidationFlows.FirstOrDefault(f => f.Slug == "default-provider-invalidation-flow")
|
|
?? AuthentikInvalidationFlows.FirstOrDefault();
|
|
|
|
SelectedSigningKeypair = string.IsNullOrWhiteSpace(AuthentikSigningKeypairId)
|
|
? AuthentikKeypairs.First() // "(none)"
|
|
: AuthentikKeypairs.FirstOrDefault(k => k.Pk == AuthentikSigningKeypairId)
|
|
?? AuthentikKeypairs.First();
|
|
|
|
// Update slug fields to match selection
|
|
if (SelectedAuthorizationFlow != null)
|
|
AuthentikAuthorizationFlowSlug = SelectedAuthorizationFlow.Slug;
|
|
if (SelectedInvalidationFlow != null)
|
|
AuthentikInvalidationFlowSlug = SelectedInvalidationFlow.Slug;
|
|
if (SelectedSigningKeypair != null)
|
|
AuthentikSigningKeypairId = SelectedSigningKeypair.Pk;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task TestXiboBootstrapAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(XiboBootstrapClientId) || string.IsNullOrWhiteSpace(XiboBootstrapClientSecret))
|
|
{
|
|
StatusMessage = "Xibo Bootstrap Client ID and Secret are required.";
|
|
return;
|
|
}
|
|
|
|
IsBusy = true;
|
|
StatusMessage = "Testing Xibo bootstrap credentials...";
|
|
try
|
|
{
|
|
using var scope = _services.CreateScope();
|
|
var xibo = scope.ServiceProvider.GetRequiredService<XiboApiService>();
|
|
var svc = scope.ServiceProvider.GetRequiredService<SettingsService>();
|
|
var cmsServerTemplate = await svc.GetAsync(SettingsService.DefaultCmsServerNameTemplate, "app.ots-signs.com");
|
|
// Use a placeholder URL — user must configure a live instance for full test
|
|
StatusMessage = "Xibo bootstrap credentials saved. Connect test requires a live instance URL — use 'Details' on an instance to verify.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"Error: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task TestMySqlConnectionAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(MySqlHost) || string.IsNullOrWhiteSpace(MySqlAdminUser))
|
|
{
|
|
StatusMessage = "MySQL Host and Admin User are required for connection test.";
|
|
return;
|
|
}
|
|
|
|
IsBusy = true;
|
|
StatusMessage = "Testing MySQL connection...";
|
|
try
|
|
{
|
|
if (!int.TryParse(MySqlPort, out var port))
|
|
port = 3306;
|
|
|
|
var docker = _services.GetRequiredService<IDockerCliService>();
|
|
var (connection, tunnel) = await docker.OpenMySqlConnectionAsync(
|
|
MySqlHost, port, MySqlAdminUser, MySqlAdminPassword);
|
|
await using var _ = connection;
|
|
using var __ = tunnel;
|
|
|
|
await using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = "SELECT 1";
|
|
await cmd.ExecuteScalarAsync();
|
|
|
|
StatusMessage = $"MySQL connection successful ({MySqlHost}:{port} via SSH tunnel).";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"MySQL connection failed: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Persists Bitwarden bootstrap credentials to appsettings.json so they survive restarts.
|
|
/// </summary>
|
|
private async Task SaveBitwardenConfigToFileAsync()
|
|
{
|
|
var path = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
|
|
var json = await File.ReadAllTextAsync(path);
|
|
var doc = JsonNode.Parse(json, documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip })!;
|
|
|
|
var bw = doc["Bitwarden"]?.AsObject();
|
|
if (bw == null)
|
|
{
|
|
bw = new JsonObject();
|
|
doc.AsObject()["Bitwarden"] = bw;
|
|
}
|
|
|
|
bw["IdentityUrl"] = BitwardenIdentityUrl;
|
|
bw["ApiUrl"] = BitwardenApiUrl;
|
|
bw["AccessToken"] = BitwardenAccessToken;
|
|
bw["OrganizationId"] = BitwardenOrganizationId;
|
|
bw["ProjectId"] = BitwardenProjectId;
|
|
bw["InstanceProjectId"] = BitwardenInstanceProjectId;
|
|
|
|
var options = new JsonSerializerOptions { WriteIndented = true };
|
|
await File.WriteAllTextAsync(path, doc.ToJsonString(options));
|
|
}
|
|
|
|
private static string? NullIfEmpty(string? value)
|
|
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
}
|