using System.Text.Json; using Lagerbestand.Data; using Lagerbestand.Models; using Microsoft.EntityFrameworkCore; namespace Lagerbestand.Services; public interface ISiteSettingsService { Task GetPromoBannerSettingsAsync(); Task SavePromoBannerSettingsAsync(PromoBannerSettings settings); Task GetLexwareApiSettingsAsync(); Task SaveLexwareApiSettingsAsync(LexwareApiSettings settings); Task GetTwoFactorSettingsAsync(); Task SaveTwoFactorSettingsAsync(TwoFactorSettings settings); Task GetSmtpSettingsAsync(); Task SaveSmtpSettingsAsync(SmtpSettings settings); Task GetSessionSettingsAsync(); Task SaveSessionSettingsAsync(SessionSettings settings); /// /// Gets a raw setting value by key /// Task GetValueAsync(string key); /// /// Sets a raw setting value by key /// Task SetValueAsync(string key, string value); } public class SiteSettingsService : ISiteSettingsService { private const string PromoBannerKey = "PromoBanner"; private const string LexwareApiKey = "LexwareApi"; private const string TwoFactorKey = "TwoFactor"; private const string SmtpKey = "Smtp"; private const string SessionKey = "Session"; private readonly AppDbContext _context; private readonly ILogger _logger; public SiteSettingsService(AppDbContext context, ILogger logger) { _context = context; _logger = logger; } public async Task GetPromoBannerSettingsAsync() { try { // Quick timeout to prevent blocking app startup using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var setting = await _context.SiteSettings .AsNoTracking() .FirstOrDefaultAsync(s => s.Key == PromoBannerKey, cts.Token); if (setting == null) { return new PromoBannerSettings(); } var settings = JsonSerializer.Deserialize(setting.Value); return settings ?? new PromoBannerSettings(); } catch (OperationCanceledException) { _logger.LogDebug("Banner settings query timed out, returning default"); return new PromoBannerSettings(); } catch (MySqlConnector.MySqlException ex) when (ex.Number == 1146) { // Table doesn't exist yet - this is normal during first startup _logger.LogDebug("SiteSettings table not found, returning default settings"); return new PromoBannerSettings(); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to load promo banner settings"); return new PromoBannerSettings(); } } public async Task SavePromoBannerSettingsAsync(PromoBannerSettings settings) { try { var existing = await _context.SiteSettings .FirstOrDefaultAsync(s => s.Key == PromoBannerKey); var json = JsonSerializer.Serialize(settings); if (existing != null) { existing.Value = json; existing.UpdatedAt = DateTime.UtcNow; _context.SiteSettings.Update(existing); } else { var newSetting = new SiteSettings { Key = PromoBannerKey, Value = json, CreatedAt = DateTime.UtcNow }; _context.SiteSettings.Add(newSetting); } await _context.SaveChangesAsync(); _logger.LogInformation("Promo banner settings saved successfully"); } catch (Exception ex) { _logger.LogError(ex, "Failed to save promo banner settings"); throw; } } public async Task GetLexwareApiSettingsAsync() { try { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var setting = await _context.SiteSettings .AsNoTracking() .FirstOrDefaultAsync(s => s.Key == LexwareApiKey, cts.Token); if (setting == null) return new LexwareApiSettings(); var settings = JsonSerializer.Deserialize(setting.Value); return settings ?? new LexwareApiSettings(); } catch (OperationCanceledException) { _logger.LogDebug("Lexware settings query timed out, returning default"); return new LexwareApiSettings(); } catch (MySqlConnector.MySqlException ex) when (ex.Number == 1146) { _logger.LogDebug("SiteSettings table not found, returning default Lexware settings"); return new LexwareApiSettings(); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to load Lexware API settings"); return new LexwareApiSettings(); } } public async Task SaveLexwareApiSettingsAsync(LexwareApiSettings settings) { try { var existing = await _context.SiteSettings .FirstOrDefaultAsync(s => s.Key == LexwareApiKey); var json = JsonSerializer.Serialize(settings); if (existing != null) { existing.Value = json; existing.UpdatedAt = DateTime.UtcNow; _context.SiteSettings.Update(existing); } else { var newSetting = new SiteSettings { Key = LexwareApiKey, Value = json, CreatedAt = DateTime.UtcNow }; _context.SiteSettings.Add(newSetting); } await _context.SaveChangesAsync(); _logger.LogInformation("Lexware API settings saved successfully"); } catch (Exception ex) { _logger.LogError(ex, "Failed to save Lexware API settings"); throw; } } public async Task GetTwoFactorSettingsAsync() { try { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var setting = await _context.SiteSettings .AsNoTracking() .FirstOrDefaultAsync(s => s.Key == TwoFactorKey, cts.Token); if (setting == null) return new TwoFactorSettings(); return JsonSerializer.Deserialize(setting.Value) ?? new TwoFactorSettings(); } catch (OperationCanceledException) { _logger.LogDebug("TwoFactor settings query timed out, returning default"); return new TwoFactorSettings(); } catch (MySqlConnector.MySqlException ex) when (ex.Number == 1146) { return new TwoFactorSettings(); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to load TwoFactor settings"); return new TwoFactorSettings(); } } public async Task SaveTwoFactorSettingsAsync(TwoFactorSettings settings) { try { var existing = await _context.SiteSettings .FirstOrDefaultAsync(s => s.Key == TwoFactorKey); var json = JsonSerializer.Serialize(settings); if (existing != null) { existing.Value = json; existing.UpdatedAt = DateTime.UtcNow; _context.SiteSettings.Update(existing); } else { _context.SiteSettings.Add(new SiteSettings { Key = TwoFactorKey, Value = json, CreatedAt = DateTime.UtcNow }); } await _context.SaveChangesAsync(); _logger.LogInformation("TwoFactor settings saved successfully"); } catch (Exception ex) { _logger.LogError(ex, "Failed to save TwoFactor settings"); throw; } } public async Task GetSmtpSettingsAsync() { try { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var setting = await _context.SiteSettings .AsNoTracking() .FirstOrDefaultAsync(s => s.Key == SmtpKey, cts.Token); if (setting == null) return new SmtpSettings(); return JsonSerializer.Deserialize(setting.Value) ?? new SmtpSettings(); } catch (OperationCanceledException) { _logger.LogDebug("SMTP settings query timed out, returning default"); return new SmtpSettings(); } catch (MySqlConnector.MySqlException ex) when (ex.Number == 1146) { return new SmtpSettings(); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to load SMTP settings"); return new SmtpSettings(); } } public async Task SaveSmtpSettingsAsync(SmtpSettings settings) { try { var existing = await _context.SiteSettings .FirstOrDefaultAsync(s => s.Key == SmtpKey); var json = JsonSerializer.Serialize(settings); if (existing != null) { existing.Value = json; existing.UpdatedAt = DateTime.UtcNow; _context.SiteSettings.Update(existing); } else { _context.SiteSettings.Add(new SiteSettings { Key = SmtpKey, Value = json, CreatedAt = DateTime.UtcNow }); } await _context.SaveChangesAsync(); _logger.LogInformation("SMTP settings saved successfully"); } catch (Exception ex) { _logger.LogError(ex, "Failed to save SMTP settings"); throw; } } public async Task GetSessionSettingsAsync() { try { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var setting = await _context.SiteSettings .AsNoTracking() .FirstOrDefaultAsync(s => s.Key == SessionKey, cts.Token); if (setting == null) return new SessionSettings(); return JsonSerializer.Deserialize(setting.Value) ?? new SessionSettings(); } catch (OperationCanceledException) { _logger.LogDebug("Session settings query timed out, returning default"); return new SessionSettings(); } catch (MySqlConnector.MySqlException ex) when (ex.Number == 1146) { return new SessionSettings(); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to load Session settings"); return new SessionSettings(); } } public async Task SaveSessionSettingsAsync(SessionSettings settings) { try { // Clamp to safe ranges before persisting settings.IdleTimeoutMinutes = settings.GetSafeIdleTimeoutMinutes(); settings.CircuitRetentionHours = settings.GetSafeCircuitRetentionHours(); var existing = await _context.SiteSettings .FirstOrDefaultAsync(s => s.Key == SessionKey); var json = JsonSerializer.Serialize(settings); if (existing != null) { existing.Value = json; existing.UpdatedAt = DateTime.UtcNow; _context.SiteSettings.Update(existing); } else { _context.SiteSettings.Add(new SiteSettings { Key = SessionKey, Value = json, CreatedAt = DateTime.UtcNow }); } await _context.SaveChangesAsync(); _logger.LogInformation("Session settings saved successfully"); } catch (Exception ex) { _logger.LogError(ex, "Failed to save Session settings"); throw; } } public async Task GetValueAsync(string key) { try { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var setting = await _context.SiteSettings .AsNoTracking() .FirstOrDefaultAsync(s => s.Key == key, cts.Token); return setting?.Value; } catch (OperationCanceledException) { _logger.LogDebug("Settings query for '{Key}' timed out", key); return null; } catch (MySqlConnector.MySqlException ex) when (ex.Number == 1146) { _logger.LogDebug("SiteSettings table not found for key '{Key}'", key); return null; } catch (Exception ex) { _logger.LogWarning(ex, "Failed to load setting '{Key}'", key); return null; } } public async Task SetValueAsync(string key, string value) { try { var existing = await _context.SiteSettings .FirstOrDefaultAsync(s => s.Key == key); if (existing != null) { existing.Value = value; existing.UpdatedAt = DateTime.UtcNow; _context.SiteSettings.Update(existing); } else { var newSetting = new SiteSettings { Key = key, Value = value, CreatedAt = DateTime.UtcNow }; _context.SiteSettings.Add(newSetting); } await _context.SaveChangesAsync(); _logger.LogInformation("Setting '{Key}' saved successfully", key); } catch (Exception ex) { _logger.LogError(ex, "Failed to save setting '{Key}'", key); throw; } } }