using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Nuuru.Server.Data; using Nuuru.Server.Models; namespace Nuuru.Server.Services { public interface ISiteSettingsService { Task GetAsync(string key); Task GetBoolAsync(string key, bool defaultValue = false); Task GetIntAsync(string key, int defaultValue = 0); Task SetAsync(string key, string value); Task> GetAllAsync(); Task IsAnonymousUploadEnabledAsync(); Task IsAnonymousCommentEnabledAsync(); Task IsRegistrationEnabledAsync(); Task GetCaptchaProviderAsync(); Task GetCaptchaSettingAsync(string key, string defaultValue); Task GetCaptchaSettingIntAsync(string key, int defaultValue); Task IsSignupGeoVerificationEnabledAsync(); Task GetSignupGeoVerificationHoldHoursAsync(); Task ShouldRejectFlaggedVpnSignupsAsync(); Task GetSignupGeoVerificationLookupUrlAsync(); Task ShouldRejectAnonymousVpnAsync(); Task IsPanicActiveAsync(); Task GetAnonymousUserIdAsync(); Task GetIntegrityAdminDashboardUrlAsync(); } public class SiteSettingsService : ISiteSettingsService { private readonly ApplicationDbContext _context; private readonly IMemoryCache _cache; private const string CachePrefix = "site_setting_"; private const string AnonUserCacheKey = "anonymous_user_id"; private static readonly MemoryCacheEntryOptions SlidingOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) }; public SiteSettingsService(ApplicationDbContext context, IMemoryCache cache) { _context = context; _cache = cache; } public async Task GetAsync(string key) { var cacheKey = CachePrefix + key; if (_cache.TryGetValue(cacheKey, out string? cached)) return cached; var setting = await _context.SiteSettings.FindAsync(key); var value = setting?.Value; _cache.Set(cacheKey, value, SlidingOptions); return value; } public async Task GetBoolAsync(string key, bool defaultValue = false) { var value = await GetAsync(key); if (value == null) return defaultValue; return bool.TryParse(value, out var result) ? result : defaultValue; } public async Task GetIntAsync(string key, int defaultValue = 0) { var value = await GetAsync(key); if (value == null) return defaultValue; return int.TryParse(value, out var result) ? result : defaultValue; } public async Task SetAsync(string key, string value) { var setting = await _context.SiteSettings.FindAsync(key); if (setting != null) { setting.Value = value; setting.UpdatedAt = DateTime.UtcNow; } else { _context.SiteSettings.Add(new SiteSetting { Key = key, Value = value, UpdatedAt = DateTime.UtcNow }); } await _context.SaveChangesAsync(); _cache.Remove(CachePrefix + key); } public async Task> GetAllAsync() { var settings = await _context.SiteSettings .ToDictionaryAsync(s => s.Key, s => s.Value); settings.TryAdd(SiteSettingKeys.RegistrationEnabled, SiteSettingKeys.DefaultRegistrationEnabled.ToString().ToLowerInvariant()); settings.TryAdd(SiteSettingKeys.AnonymousUploadsEnabled, bool.FalseString.ToLowerInvariant()); settings.TryAdd(SiteSettingKeys.AnonymousCommentsEnabled, bool.FalseString.ToLowerInvariant()); settings.TryAdd(SiteSettingKeys.AnonymousRejectFlaggedVpn, SiteSettingKeys.DefaultAnonymousRejectFlaggedVpn.ToString().ToLowerInvariant()); // Captcha defaults settings.TryAdd(SiteSettingKeys.CaptchaProvider, SiteSettingKeys.DefaultCaptchaProvider); settings.TryAdd(SiteSettingKeys.CaptchaHCaptchaSiteKey, ""); settings.TryAdd(SiteSettingKeys.CaptchaHCaptchaSecretKey, ""); settings.TryAdd(SiteSettingKeys.CaptchaHCaptchaVerifyUrl, SiteSettingKeys.DefaultCaptchaHCaptchaVerifyUrl); settings.TryAdd(SiteSettingKeys.CaptchaBuiltInImageWidth, SiteSettingKeys.DefaultCaptchaBuiltInImageWidth.ToString()); settings.TryAdd(SiteSettingKeys.CaptchaBuiltInImageHeight, SiteSettingKeys.DefaultCaptchaBuiltInImageHeight.ToString()); settings.TryAdd(SiteSettingKeys.CaptchaBuiltInChallengeTtlSeconds, SiteSettingKeys.DefaultCaptchaBuiltInChallengeTtlSeconds.ToString()); // SSR default settings.TryAdd(SiteSettingKeys.SsrEnabled, SiteSettingKeys.DefaultSsrEnabled.ToString().ToLowerInvariant()); // Signup verification defaults settings.TryAdd(SiteSettingKeys.SignupGeoVerificationEnabled, SiteSettingKeys.DefaultSignupGeoVerificationEnabled.ToString().ToLowerInvariant()); settings.TryAdd(SiteSettingKeys.SignupGeoVerificationHoldHours, SiteSettingKeys.DefaultSignupGeoVerificationHoldHours.ToString()); settings.TryAdd(SiteSettingKeys.SignupGeoVerificationRejectFlaggedVpnSignups, SiteSettingKeys.DefaultSignupGeoVerificationRejectFlaggedVpnSignups.ToString().ToLowerInvariant()); settings.TryAdd(SiteSettingKeys.SignupGeoVerificationLookupUrl, SiteSettingKeys.DefaultSignupGeoVerificationLookupUrl); // Integrity defaults settings.TryAdd(SiteSettingKeys.IntegrityAdminDashboardUrl, SiteSettingKeys.DefaultIntegrityAdminDashboardUrl); return settings; } public Task IsAnonymousUploadEnabledAsync() => GetBoolAsync(SiteSettingKeys.AnonymousUploadsEnabled); public Task IsAnonymousCommentEnabledAsync() => GetBoolAsync(SiteSettingKeys.AnonymousCommentsEnabled); public Task IsRegistrationEnabledAsync() => GetBoolAsync(SiteSettingKeys.RegistrationEnabled, SiteSettingKeys.DefaultRegistrationEnabled); public async Task GetCaptchaProviderAsync() { var value = await GetAsync(SiteSettingKeys.CaptchaProvider); if (string.IsNullOrWhiteSpace(value)) return SiteSettingKeys.DefaultCaptchaProvider; return value.Trim().ToLowerInvariant(); } public async Task GetCaptchaSettingAsync(string key, string defaultValue) { var value = await GetAsync(key); return string.IsNullOrWhiteSpace(value) ? defaultValue : value.Trim(); } public async Task GetCaptchaSettingIntAsync(string key, int defaultValue) { var value = await GetAsync(key); if (value == null) return defaultValue; return int.TryParse(value, out var result) ? result : defaultValue; } public Task IsSignupGeoVerificationEnabledAsync() => GetBoolAsync(SiteSettingKeys.SignupGeoVerificationEnabled, SiteSettingKeys.DefaultSignupGeoVerificationEnabled); public async Task GetSignupGeoVerificationHoldHoursAsync() { var holdHours = await GetIntAsync( SiteSettingKeys.SignupGeoVerificationHoldHours, SiteSettingKeys.DefaultSignupGeoVerificationHoldHours); // Keep this bounded to sane values and avoid negative holds. return Math.Clamp(holdHours, 0, 24 * 30); } public Task ShouldRejectAnonymousVpnAsync() => GetBoolAsync(SiteSettingKeys.AnonymousRejectFlaggedVpn, SiteSettingKeys.DefaultAnonymousRejectFlaggedVpn); public Task IsPanicActiveAsync() => GetBoolAsync(SiteSettingKeys.Panic, SiteSettingKeys.DefaultPanic); public Task ShouldRejectFlaggedVpnSignupsAsync() => GetBoolAsync( SiteSettingKeys.SignupGeoVerificationRejectFlaggedVpnSignups, SiteSettingKeys.DefaultSignupGeoVerificationRejectFlaggedVpnSignups); public async Task GetSignupGeoVerificationLookupUrlAsync() { var value = await GetAsync(SiteSettingKeys.SignupGeoVerificationLookupUrl); if (string.IsNullOrWhiteSpace(value)) { return SiteSettingKeys.DefaultSignupGeoVerificationLookupUrl; } return value.Trim(); } public async Task GetAnonymousUserIdAsync() { if (_cache.TryGetValue(AnonUserCacheKey, out Guid? cached)) return cached; var userId = await _context.Users .Where(u => u.IsSystemAccount) .Select(u => (Guid?)u.Id) .FirstOrDefaultAsync(); if (userId.HasValue) { _cache.Set(AnonUserCacheKey, userId, new MemoryCacheEntryOptions { Priority = CacheItemPriority.NeverRemove }); } return userId; } public async Task GetIntegrityAdminDashboardUrlAsync() { var value = await GetAsync(SiteSettingKeys.IntegrityAdminDashboardUrl); if (string.IsNullOrWhiteSpace(value)) return SiteSettingKeys.DefaultIntegrityAdminDashboardUrl; return value.Trim(); } } }