using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using OTSSignsOrchestrator.Core.Data; using OTSSignsOrchestrator.Core.Models.Entities; namespace OTSSignsOrchestrator.Core.Services; /// /// Reads and writes typed application settings from the AppSetting table. /// Sensitive values are encrypted/decrypted transparently via DataProtection. /// public class SettingsService { private readonly XiboContext _db; private readonly IDataProtector _protector; private readonly ILogger _logger; // ── Category constants ───────────────────────────────────────────────── public const string CatGit = "Git"; public const string CatMySql = "MySql"; public const string CatSmtp = "Smtp"; public const string CatPangolin = "Pangolin"; public const string CatNfs = "Nfs"; public const string CatDefaults = "Defaults"; // ── Key constants ────────────────────────────────────────────────────── // Git public const string GitRepoUrl = "Git.RepoUrl"; public const string GitRepoPat = "Git.RepoPat"; // MySQL Admin public const string MySqlHost = "MySql.Host"; public const string MySqlPort = "MySql.Port"; public const string MySqlAdminUser = "MySql.AdminUser"; public const string MySqlAdminPassword = "MySql.AdminPassword"; // SMTP public const string SmtpServer = "Smtp.Server"; public const string SmtpPort = "Smtp.Port"; public const string SmtpUsername = "Smtp.Username"; public const string SmtpPassword = "Smtp.Password"; public const string SmtpUseTls = "Smtp.UseTls"; public const string SmtpUseStartTls = "Smtp.UseStartTls"; public const string SmtpRewriteDomain = "Smtp.RewriteDomain"; public const string SmtpHostname = "Smtp.Hostname"; public const string SmtpFromLineOverride = "Smtp.FromLineOverride"; // Pangolin public const string PangolinEndpoint = "Pangolin.Endpoint"; // NFS public const string NfsServer = "Nfs.Server"; public const string NfsExport = "Nfs.Export"; public const string NfsExportFolder = "Nfs.ExportFolder"; public const string NfsOptions = "Nfs.Options"; // Instance Defaults public const string DefaultCmsImage = "Defaults.CmsImage"; public const string DefaultNewtImage = "Defaults.NewtImage"; public const string DefaultMemcachedImage = "Defaults.MemcachedImage"; public const string DefaultQuickChartImage = "Defaults.QuickChartImage"; public const string DefaultCmsServerNameTemplate = "Defaults.CmsServerNameTemplate"; public const string DefaultThemeHostPath = "Defaults.ThemeHostPath"; public const string DefaultMySqlDbTemplate = "Defaults.MySqlDbTemplate"; public const string DefaultMySqlUserTemplate = "Defaults.MySqlUserTemplate"; public const string DefaultPhpPostMaxSize = "Defaults.PhpPostMaxSize"; public const string DefaultPhpUploadMaxFilesize = "Defaults.PhpUploadMaxFilesize"; public const string DefaultPhpMaxExecutionTime = "Defaults.PhpMaxExecutionTime"; // Instance-specific (keyed by abbreviation) /// /// Builds a per-instance settings key for the MySQL password. /// Stored encrypted via DataProtection so it can be retrieved on update/redeploy. /// public static string InstanceMySqlPassword(string abbrev) => $"Instance.{abbrev}.MySqlPassword"; public const string CatInstance = "Instance"; public SettingsService( XiboContext db, IDataProtectionProvider dataProtection, ILogger logger) { _db = db; _protector = dataProtection.CreateProtector("OTSSignsOrchestrator.Settings"); _logger = logger; } /// Get a single setting value, decrypting if sensitive. public async Task GetAsync(string key) { var setting = await _db.AppSettings.FindAsync(key); if (setting == null) return null; return setting.IsSensitive && setting.Value != null ? Unprotect(setting.Value) : setting.Value; } /// Get a setting with a fallback default. public async Task GetAsync(string key, string defaultValue) => await GetAsync(key) ?? defaultValue; /// Set a single setting, encrypting if sensitive. public async Task SetAsync(string key, string? value, string category, bool isSensitive = false) { var setting = await _db.AppSettings.FindAsync(key); if (setting == null) { setting = new AppSetting { Key = key, Category = category, IsSensitive = isSensitive }; _db.AppSettings.Add(setting); } setting.Value = isSensitive && value != null ? _protector.Protect(value) : value; setting.IsSensitive = isSensitive; setting.Category = category; setting.UpdatedAt = DateTime.UtcNow; } /// Save multiple settings in a single transaction. public async Task SaveManyAsync(IEnumerable<(string Key, string? Value, string Category, bool IsSensitive)> settings) { foreach (var (key, value, category, isSensitive) in settings) await SetAsync(key, value, category, isSensitive); await _db.SaveChangesAsync(); _logger.LogInformation("Saved {Count} setting(s)", settings is ICollection<(string, string?, string, bool)> c ? c.Count : -1); } /// Get all settings in a category (values decrypted). public async Task> GetCategoryAsync(string category) { var settings = await _db.AppSettings .Where(s => s.Category == category) .ToListAsync(); return settings.ToDictionary( s => s.Key, s => s.IsSensitive && s.Value != null ? Unprotect(s.Value) : s.Value); } private string? Unprotect(string protectedValue) { try { return _protector.Unprotect(protectedValue); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to unprotect setting value — returning null"); return null; } } }