using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; using UtopiaCanteenSystem.Data; namespace UtopiaCanteenSystem.Services; /// /// JSON-backed configuration (including remember-me credentials) stored under /// LocalApplicationData so it survives app updates when deployed from a file server. /// public class ConfigService : IConfigService { private readonly string _configPath; private AppConfig _config; public ConfigService() { _configPath = DatabasePath.GetConfigPath(); _config = LoadConfig(); } public string GetSyncApiEndpoint() => _config.SyncApiEndpoint ?? string.Empty; public void SetSyncApiEndpoint(string endpoint) { _config.SyncApiEndpoint = endpoint ?? string.Empty; SaveConfig(); } public bool GetScannerConnected() => _config.ScannerConnected; public void SetScannerConnected(bool connected) { _config.ScannerConnected = connected; SaveConfig(); } private const int ScanIntervalDaysMax = 365; private const int ScanIntervalHoursMax = 23; private const int ScanIntervalMinutesMax = 59; private const int ScanIntervalSecondsMax = 59; private static readonly TimeSpan ScanIntervalFallback = TimeSpan.FromMinutes(1); public int GetScanIntervalDays() => Math.Clamp(_config.ScanIntervalDays, 0, ScanIntervalDaysMax); public void SetScanIntervalDays(int value) { _config.ScanIntervalDays = Math.Clamp(value, 0, ScanIntervalDaysMax); SaveConfig(); } public int GetScanIntervalHours() => Math.Clamp(_config.ScanIntervalHours, 0, ScanIntervalHoursMax); public void SetScanIntervalHours(int value) { _config.ScanIntervalHours = Math.Clamp(value, 0, ScanIntervalHoursMax); SaveConfig(); } public int GetScanIntervalMinutes() => Math.Clamp(_config.ScanIntervalMinutes, 0, ScanIntervalMinutesMax); public void SetScanIntervalMinutes(int value) { _config.ScanIntervalMinutes = Math.Clamp(value, 0, ScanIntervalMinutesMax); SaveConfig(); } public int GetScanIntervalSeconds() => Math.Clamp(_config.ScanIntervalSeconds, 0, ScanIntervalSecondsMax); public void SetScanIntervalSeconds(int value) { _config.ScanIntervalSeconds = Math.Clamp(value, 0, ScanIntervalSecondsMax); SaveConfig(); } public TimeSpan GetScanInterval() { var days = Math.Clamp(_config.ScanIntervalDays, 0, ScanIntervalDaysMax); var hours = Math.Clamp(_config.ScanIntervalHours, 0, ScanIntervalHoursMax); var minutes = Math.Clamp(_config.ScanIntervalMinutes, 0, ScanIntervalMinutesMax); var seconds = Math.Clamp(_config.ScanIntervalSeconds, 0, ScanIntervalSecondsMax); var ts = TimeSpan.FromDays(days) + TimeSpan.FromHours(hours) + TimeSpan.FromMinutes(minutes) + TimeSpan.FromSeconds(seconds); return ts > TimeSpan.Zero ? ts : ScanIntervalFallback; } public string GetAdminCardId() => _config.AdminCardId ?? string.Empty; public void SetAdminCardId(string cardId) { _config.AdminCardId = cardId ?? string.Empty; SaveConfig(); } public string GetSiteId() { // Return the configured SiteId as-is. No normalization. // Returns empty string if not configured. return _config.SiteId ?? string.Empty; } public void SetSiteId(string siteId) { // Store the SiteId exactly as provided. No automatic normalization. // Caller is responsible for formatting. _config.SiteId = siteId ?? string.Empty; SaveConfig(); } public void ApplyLocationSiteIdFromAuth(string? locationSiteId) { if (string.IsNullOrWhiteSpace(locationSiteId)) return; var digits = new string(locationSiteId.Where(char.IsDigit).ToArray()); if (string.IsNullOrEmpty(digits)) return; SetSiteId("SITE : " + digits); } public string GetDeviceId() { //var id = _config.DeviceId ?? string.Empty; var id = Environment.MachineName; if (string.IsNullOrWhiteSpace(id)) { id = Guid.NewGuid().ToString("N"); _config.DeviceId = id; SaveConfig(); } return id; } public void SetDeviceId(string deviceId) { _config.DeviceId = deviceId ?? string.Empty; SaveConfig(); } public bool GetRememberAdminCredentials() => _config.RememberAdminCredentials; public void SetRememberAdminCredentials(bool remember) { _config.RememberAdminCredentials = remember; SaveConfig(); } public string GetSavedAdminUsername() => _config.SavedAdminUsername ?? string.Empty; public void SetSavedAdminUsername(string username) { _config.SavedAdminUsername = username ?? string.Empty; SaveConfig(); } public string GetSavedAdminPassword() { if (!_config.RememberAdminCredentials) return string.Empty; var protectedValue = _config.SavedAdminPasswordProtected ?? string.Empty; if (string.IsNullOrWhiteSpace(protectedValue)) return string.Empty; try { var bytes = Convert.FromBase64String(protectedValue); var clear = ProtectedData.Unprotect(bytes, null, DataProtectionScope.CurrentUser); return Encoding.UTF8.GetString(clear); } catch { return string.Empty; } } public void SetSavedAdminPassword(string password) { if (!_config.RememberAdminCredentials) { _config.SavedAdminPasswordProtected = string.Empty; SaveConfig(); return; } try { var clear = Encoding.UTF8.GetBytes(password ?? string.Empty); var protectedBytes = ProtectedData.Protect(clear, null, DataProtectionScope.CurrentUser); _config.SavedAdminPasswordProtected = Convert.ToBase64String(protectedBytes); } catch { _config.SavedAdminPasswordProtected = string.Empty; } SaveConfig(); } /// /// Returns MySQL connection string for sync. Set via appsettings.json (copy from appsettings.example.json). /// No default credentials; returns empty if not configured. /// public string GetMySqlConnectionString() => (_config.MySqlConnectionString ?? string.Empty).Trim(); public void SetMySqlConnectionString(string connectionString) { _config.MySqlConnectionString = connectionString ?? string.Empty; SaveConfig(); } //public string GetHrmsLookupConnectionString() => (_config.HrmsLookupConnectionString ?? string.Empty).Trim(); public string GetHrmsLookupConnectionString() { var hrms = (_config.HrmsLookupConnectionString ?? string.Empty).Trim(); if (!string.IsNullOrWhiteSpace(hrms)) return hrms; // fallback to production/AWS connection string return GetMySqlConnectionString(); } public void SetHrmsLookupConnectionString(string connectionString) { _config.HrmsLookupConnectionString = connectionString ?? string.Empty; SaveConfig(); } private AppConfig LoadConfig() { try { if (!File.Exists(_configPath)) { System.Diagnostics.Debug.WriteLine("[ConfigService] Config file not found. Creating with defaults."); var defaults = new AppConfig(); SaveConfig(defaults); return defaults; } var json = File.ReadAllText(_configPath); var config = JsonSerializer.Deserialize(json); config ??= new AppConfig(); MigrateScanIntervalFromSecondsIfNeeded(config); System.Diagnostics.Debug.WriteLine($"[ConfigService] Config loaded from {_configPath}"); System.Diagnostics.Debug.WriteLine($"[ConfigService] SiteId: '{config.SiteId}' (empty=not configured)"); System.Diagnostics.Debug.WriteLine($"[ConfigService] MySqlConnectionString: {(string.IsNullOrWhiteSpace(config.MySqlConnectionString) ? "NOT CONFIGURED" : MaskConnectionString(config.MySqlConnectionString))}"); System.Diagnostics.Debug.WriteLine($"[ConfigService] HrmsLookupConnectionString: {(string.IsNullOrWhiteSpace(config.HrmsLookupConnectionString) ? "NOT CONFIGURED" : MaskConnectionString(config.HrmsLookupConnectionString))}"); return config; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[ConfigService] ERROR loading config: {ex.Message}"); return new AppConfig(); } } /// /// One-time migration: if old ScanTimeoutSeconds is set and new D/H/M are all zero, convert and persist. /// private void MigrateScanIntervalFromSecondsIfNeeded(AppConfig config) { var legacySeconds = config.ScanTimeoutSeconds; if (legacySeconds <= 0) return; var d = config.ScanIntervalDays; var h = config.ScanIntervalHours; var m = config.ScanIntervalMinutes; var s = config.ScanIntervalSeconds; if (d != 0 || h != 0 || m != 0 || s != 0) return; // Already using new format var totalSeconds = Math.Max(0, legacySeconds); var days = Math.Min(totalSeconds / 86400, ScanIntervalDaysMax); var remainder = totalSeconds % 86400; var hours = Math.Min(remainder / 3600, ScanIntervalHoursMax); remainder %= 3600; var minutes = Math.Min(remainder / 60, ScanIntervalMinutesMax); var seconds = Math.Min(remainder % 60, ScanIntervalSecondsMax); if (days == 0 && hours == 0 && minutes == 0 && seconds == 0) seconds = 1; config.ScanIntervalDays = days; config.ScanIntervalHours = hours; config.ScanIntervalMinutes = minutes; config.ScanIntervalSeconds = seconds; config.ScanTimeoutSeconds = 0; _config = config; SaveConfig(config); } private void SaveConfig() { SaveConfig(_config); } private void SaveConfig(AppConfig config) { try { // Log what's being saved System.Diagnostics.Debug.WriteLine($"[ConfigService] Saving config..."); System.Diagnostics.Debug.WriteLine($"[ConfigService] SiteId: '{_config.SiteId}'"); System.Diagnostics.Debug.WriteLine($"[ConfigService] MySqlConnectionString: {(string.IsNullOrWhiteSpace(_config.MySqlConnectionString) ? "NOT CONFIGURED" : MaskConnectionString(_config.MySqlConnectionString))}"); System.Diagnostics.Debug.WriteLine($"[ConfigService] HrmsLookupConnectionString: {(string.IsNullOrWhiteSpace(_config.HrmsLookupConnectionString) ? "NOT CONFIGURED" : MaskConnectionString(_config.HrmsLookupConnectionString))}"); var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(_configPath, json); System.Diagnostics.Debug.WriteLine($"[ConfigService] Config saved to {_configPath}"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[ConfigService] ERROR saving config: {ex.Message}"); } } /// /// Masks sensitive connection string values for logging. /// Shows server and database but hides password. /// private static string MaskConnectionString(string connectionString) { if (string.IsNullOrWhiteSpace(connectionString)) return string.Empty; // Simple masking: show everything before 'Password=' and mask the rest var passwordIndex = connectionString.IndexOf("Password=", StringComparison.OrdinalIgnoreCase); if (passwordIndex >= 0) { var semicolonAfterPassword = connectionString.IndexOf(';', passwordIndex); if (semicolonAfterPassword >= 0) { return connectionString.Substring(0, passwordIndex) + "Password=***" + connectionString.Substring(semicolonAfterPassword); } return connectionString.Substring(0, passwordIndex) + "Password=***"; } return connectionString; // No password found, return as-is } private sealed class AppConfig { public string SyncApiEndpoint { get; set; } = "https://api.example.com/uind/sync"; public bool ScannerConnected { get; set; } = false; /// Legacy: migrated to ScanIntervalDays/Hours/Minutes on first load. Kept for JSON deserialization. public int ScanTimeoutSeconds { get; set; } = 0; public int ScanIntervalDays { get; set; } = 0; public int ScanIntervalHours { get; set; } = 0; public int ScanIntervalMinutes { get; set; } = 0; public int ScanIntervalSeconds { get; set; } = 2; public string AdminCardId { get; set; } = "ADMIN"; public string SiteId { get; set; } = string.Empty; // No default - must be explicitly configured public string DeviceId { get; set; } = string.Empty; // Admin credential persistence (optional). public bool RememberAdminCredentials { get; set; } = false; public string SavedAdminUsername { get; set; } = string.Empty; public string SavedAdminPasswordProtected { get; set; } = string.Empty; // MySQL connection string for sync. Set in appsettings.json (see appsettings.example.json). No default. public string MySqlConnectionString { get; set; } = string.Empty; // Local HRMS MySQL for employee lookup by RFID. Separate from production sync. public string HrmsLookupConnectionString { get; set; } = string.Empty; } }