From bf1f8accd1ff877b9e7297eedbd5c067cba51030 Mon Sep 17 00:00:00 2001 From: Syed Mustafa Ahmed Naqvi Date: Sat, 23 May 2026 12:55:06 +0500 Subject: [PATCH] Split server and client configuration handling Adds client-safe configuration storage, backend base URL support, shared connection string fallback logic, and updated config path handling for separate backend/client deployments. --- Data/DatabasePath.cs | 20 +++- Services/ClientConfigService.cs | 181 +++++++++++++++++++++++++++++ Services/ConfigConnectionHelper.cs | 17 +++ Services/ConfigService.cs | 59 +++++++--- Services/IConfigService.cs | 11 +- 5 files changed, 271 insertions(+), 17 deletions(-) create mode 100644 Services/ClientConfigService.cs create mode 100644 Services/ConfigConnectionHelper.cs diff --git a/Data/DatabasePath.cs b/Data/DatabasePath.cs index 2bb96cb..70cfe5a 100644 --- a/Data/DatabasePath.cs +++ b/Data/DatabasePath.cs @@ -10,6 +10,24 @@ public static class DatabasePath { private static string? _appDataFolder; private static string? _dbPath; + private static string _productFolderName = "UtopiaCanteenSystem"; + + /// + /// Use "UtopiaCanteenBackend" for the Windows Service; default is legacy WPF folder name. + /// + public static void UseBackendServiceStorage() + { + _productFolderName = "UtopiaCanteenBackend"; + _appDataFolder = null; + _dbPath = null; + } + + public static void UseClientStorage() + { + _productFolderName = "UtopiaCanteenClient"; + _appDataFolder = null; + _dbPath = null; + } /// /// Folder under LocalApplicationData for DB and config. Created on first use. @@ -21,7 +39,7 @@ public static class DatabasePath _appDataFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "UtopiaCanteenSystem"); + _productFolderName); Directory.CreateDirectory(_appDataFolder); return _appDataFolder; } diff --git a/Services/ClientConfigService.cs b/Services/ClientConfigService.cs new file mode 100644 index 0000000..f4770a2 --- /dev/null +++ b/Services/ClientConfigService.cs @@ -0,0 +1,181 @@ +using System.IO; +using System.Text.Json; +using UtopiaCanteenSystem.Data; +using UtopiaCanteenSystem.Models; + +namespace UtopiaCanteenSystem.Services; + +/// +/// Scanner frontend configuration: backend URL, device/site, admin UI. No HRMS/production credentials. +/// +public sealed class ClientConfigService : IConfigService +{ + private readonly string _configPath; + private ClientAppConfig _config; + + public ClientConfigService() + { + DatabasePath.UseClientStorage(); + _configPath = DatabasePath.GetConfigPath(); + _config = LoadConfig(); + } + + public string GetSyncApiEndpoint() => string.Empty; + public void SetSyncApiEndpoint(string endpoint) { } + + public bool GetSyncServiceEnabled() => false; + public void SetSyncServiceEnabled(bool enabled) { } + + public bool GetScannerConnected() => _config.ScannerConnected; + public void SetScannerConnected(bool connected) + { + _config.ScannerConnected = connected; + SaveConfig(); + } + + public int GetScanIntervalDays() => Math.Clamp(_config.ScanIntervalDays, 0, 365); + public void SetScanIntervalDays(int value) { _config.ScanIntervalDays = Math.Clamp(value, 0, 365); SaveConfig(); } + public int GetScanIntervalHours() => Math.Clamp(_config.ScanIntervalHours, 0, 23); + public void SetScanIntervalHours(int value) { _config.ScanIntervalHours = Math.Clamp(value, 0, 23); SaveConfig(); } + public int GetScanIntervalMinutes() => Math.Clamp(_config.ScanIntervalMinutes, 0, 59); + public void SetScanIntervalMinutes(int value) { _config.ScanIntervalMinutes = Math.Clamp(value, 0, 59); SaveConfig(); } + public int GetScanIntervalSeconds() => Math.Clamp(_config.ScanIntervalSeconds, 0, 59); + public void SetScanIntervalSeconds(int value) { _config.ScanIntervalSeconds = Math.Clamp(value, 0, 59); SaveConfig(); } + + public TimeSpan GetScanInterval() + { + var ts = TimeSpan.FromDays(GetScanIntervalDays()) + TimeSpan.FromHours(GetScanIntervalHours()) + + TimeSpan.FromMinutes(GetScanIntervalMinutes()) + TimeSpan.FromSeconds(GetScanIntervalSeconds()); + return ts > TimeSpan.Zero ? ts : TimeSpan.FromMinutes(1); + } + + public string GetAdminCardId() => _config.AdminCardId ?? "ADMIN"; + public void SetAdminCardId(string cardId) { _config.AdminCardId = cardId ?? string.Empty; SaveConfig(); } + + public string GetSiteId() => _config.SiteId ?? string.Empty; + public void SetSiteId(string siteId) { _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; + _config.SiteId = $"SITE : {digits}"; + SaveConfig(); + } + + public string GetDeviceId() => _config.DeviceId ?? string.Empty; + 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() => DecryptPassword(_config.SavedAdminPasswordProtected ?? string.Empty); + public void SetSavedAdminPassword(string password) { _config.SavedAdminPasswordProtected = EncryptPassword(password ?? string.Empty); SaveConfig(); } + + public string GetMySqlConnectionString() => string.Empty; + public void SetMySqlConnectionString(string connectionString) { } + + public string GetHrmsLookupConnectionString() => string.Empty; + public void SetHrmsLookupConnectionString(string connectionString) { } + + public DateTime? GetLastEmployeeRfidCacheSyncUtc() => null; + public void SetLastEmployeeRfidCacheSyncUtc(DateTime utc) { } + + public DateTime? GetLastMealMenuCacheSyncUtc() => null; + public void SetLastMealMenuCacheSyncUtc(DateTime utc) { } + + public AppMode GetAppMode() => AppMode.Client; + public void SetAppMode(AppMode mode) { } + + public string GetCentralServerBaseUrl() => GetBackendBaseUrl(); + public void SetCentralServerBaseUrl(string url) => SetBackendBaseUrl(url); + + public string GetBackendBaseUrl() => (_config.BackendBaseUrl ?? string.Empty).TrimEnd('/'); + + public void SetBackendBaseUrl(string url) + { + _config.BackendBaseUrl = (url ?? string.Empty).Trim().TrimEnd('/'); + SaveConfig(); + } + + public string GetLocalServerListenUrls() => string.Empty; + public void SetLocalServerListenUrls(string urls) { } + + private ClientAppConfig LoadConfig() + { + try + { + if (!File.Exists(_configPath)) + return new ClientAppConfig { BackendBaseUrl = "http://localhost:5000" }; + var json = File.ReadAllText(_configPath); + return JsonSerializer.Deserialize(json) ?? new ClientAppConfig(); + } + catch + { + return new ClientAppConfig(); + } + } + + private void SaveConfig() + { + try + { + var json = JsonSerializer.Serialize(_config, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(_configPath, json); + } + catch (Exception ex) + { + Logger.Log(ex, "ClientConfigService.SaveConfig"); + } + } + + private static string EncryptPassword(string plain) + { + if (string.IsNullOrEmpty(plain)) return string.Empty; + try + { + var bytes = System.Text.Encoding.UTF8.GetBytes(plain); + var protectedBytes = System.Security.Cryptography.ProtectedData.Protect(bytes, null, System.Security.Cryptography.DataProtectionScope.CurrentUser); + return Convert.ToBase64String(protectedBytes); + } + catch + { + return string.Empty; + } + } + + private static string DecryptPassword(string protectedBase64) + { + if (string.IsNullOrEmpty(protectedBase64)) return string.Empty; + try + { + var protectedBytes = Convert.FromBase64String(protectedBase64); + var bytes = System.Security.Cryptography.ProtectedData.Unprotect(protectedBytes, null, System.Security.Cryptography.DataProtectionScope.CurrentUser); + return System.Text.Encoding.UTF8.GetString(bytes); + } + catch + { + return string.Empty; + } + } + + private sealed class ClientAppConfig + { + public string? BackendBaseUrl { get; set; } + public bool ScannerConnected { get; set; } + public int ScanIntervalDays { get; set; } + public int ScanIntervalHours { get; set; } + public int ScanIntervalMinutes { get; set; } + public int ScanIntervalSeconds { get; set; } = 5; + public string? AdminCardId { get; set; } + public string? SiteId { get; set; } + public string? DeviceId { get; set; } + public bool RememberAdminCredentials { get; set; } + public string? SavedAdminUsername { get; set; } + public string? SavedAdminPasswordProtected { get; set; } + } +} diff --git a/Services/ConfigConnectionHelper.cs b/Services/ConfigConnectionHelper.cs new file mode 100644 index 0000000..e7fdb82 --- /dev/null +++ b/Services/ConfigConnectionHelper.cs @@ -0,0 +1,17 @@ +namespace UtopiaCanteenSystem.Services; + +/// +/// Resolves the effective HRMS/UIND MySQL connection: explicit HRMS lookup string when set, +/// otherwise the production value. +/// +public static class ConfigConnectionHelper +{ + public static string GetHrmsOrProductionConnectionString(string? hrmsLookupConnectionString, string? mySqlConnectionString) + { + var hrms = (hrmsLookupConnectionString ?? string.Empty).Trim(); + if (!string.IsNullOrWhiteSpace(hrms)) + return hrms; + + return (mySqlConnectionString ?? string.Empty).Trim(); + } +} diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs index bef6fac..c995cfc 100644 --- a/Services/ConfigService.cs +++ b/Services/ConfigService.cs @@ -218,17 +218,10 @@ public class ConfigService : IConfigService 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 string GetHrmsLookupConnectionString() => + ConfigConnectionHelper.GetHrmsOrProductionConnectionString( + _config.HrmsLookupConnectionString, + _config.MySqlConnectionString); public void SetHrmsLookupConnectionString(string connectionString) { @@ -270,7 +263,9 @@ public class ConfigService : IConfigService public void SetCentralServerBaseUrl(string url) { - _config.CentralServerBaseUrl = url?.Trim() ?? string.Empty; + var v = url?.Trim() ?? string.Empty; + _config.CentralServerBaseUrl = v; + _config.BackendBaseUrl = v; SaveConfig(); } @@ -286,6 +281,37 @@ public class ConfigService : IConfigService SaveConfig(); } + public string GetBackendBaseUrl() + { + if (GetAppMode() == AppMode.Server) + return DeriveLocalhostApiBaseUrl(GetLocalServerListenUrls()); + + var url = (_config.BackendBaseUrl ?? string.Empty).Trim(); + if (!string.IsNullOrEmpty(url)) + return url.TrimEnd('/'); + + return GetCentralServerBaseUrl().TrimEnd('/'); + } + + private static string DeriveLocalhostApiBaseUrl(string listenUrls) + { + var first = (listenUrls ?? string.Empty) + .Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .FirstOrDefault() ?? "http://0.0.0.0:5000"; + + first = first.Trim().TrimEnd('/'); + if (first.Contains("0.0.0.0", StringComparison.Ordinal)) + first = first.Replace("0.0.0.0", "localhost", StringComparison.Ordinal); + if (first.Contains('+')) + first = first.Replace("+", "localhost", StringComparison.Ordinal); + + if (!first.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && + !first.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + first = "http://" + first; + + return first; + } + private AppConfig LoadConfig() { try @@ -302,6 +328,8 @@ public class ConfigService : IConfigService var json = File.ReadAllText(_configPath); var config = JsonSerializer.Deserialize(json); config ??= new AppConfig(); + if (string.IsNullOrWhiteSpace(config.BackendBaseUrl) && !string.IsNullOrWhiteSpace(config.CentralServerBaseUrl)) + config.BackendBaseUrl = config.CentralServerBaseUrl; MigrateScanIntervalFromSecondsIfNeeded(config); var changed = EnsureConnectionStringDefaults(config); if (changed) @@ -450,10 +478,13 @@ public class ConfigService : IConfigService /// Server (default) or Client. public string AppMode { get; set; } = "Server"; - /// Scanner PCs: base URL of central app API (e.g. http://192.168.1.10:5000). + /// Legacy name; same as BackendBaseUrl for client PCs. public string CentralServerBaseUrl { get; set; } = string.Empty; - /// Central PC: Kestrel listen URL(s), e.g. http://0.0.0.0:5000 + /// Client: remote backend URL. Server: optional override (defaults to localhost from ListenUrls). + public string BackendBaseUrl { get; set; } = string.Empty; + + /// Central PC: API listen URL(s), e.g. http://0.0.0.0:5000 public string LocalServerListenUrls { get; set; } = "http://0.0.0.0:5000"; } } diff --git a/Services/IConfigService.cs b/Services/IConfigService.cs index 1460e04..408d135 100644 --- a/Services/IConfigService.cs +++ b/Services/IConfigService.cs @@ -44,7 +44,10 @@ public interface IConfigService string GetMySqlConnectionString(); void SetMySqlConnectionString(string connectionString); - /// Connection string for local HRMS MySQL (employee lookup by RFID). Separate from production sync. + /// + /// MySQL connection for HRMS/UIND (cache sync, employee lookup, lunch_order post). + /// Uses HrmsLookupConnectionString when set; otherwise . + /// string GetHrmsLookupConnectionString(); void SetHrmsLookupConnectionString(string connectionString); @@ -57,10 +60,14 @@ public interface IConfigService AppMode GetAppMode(); void SetAppMode(AppMode mode); + /// Client mode: central server API base URL (e.g. http://192.168.1.10:5000). string GetCentralServerBaseUrl(); void SetCentralServerBaseUrl(string url); - /// Kestrel listen URL(s) when (e.g. http://0.0.0.0:5000). + /// Backend API base URL for HTTP calls (client: remote server; server: http://localhost:port). + string GetBackendBaseUrl(); + + /// HttpListener bind URL(s) when (e.g. http://0.0.0.0:5000). string GetLocalServerListenUrls(); void SetLocalServerListenUrls(string urls); }