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.
feature/centralized-offline-canteen
SYED MUSTUFA AHMED NAQVI 2026-05-23 12:55:06 +05:00
parent 891ddc9175
commit bf1f8accd1
5 changed files with 271 additions and 17 deletions

View File

@ -10,6 +10,24 @@ public static class DatabasePath
{ {
private static string? _appDataFolder; private static string? _appDataFolder;
private static string? _dbPath; private static string? _dbPath;
private static string _productFolderName = "UtopiaCanteenSystem";
/// <summary>
/// Use "UtopiaCanteenBackend" for the Windows Service; default is legacy WPF folder name.
/// </summary>
public static void UseBackendServiceStorage()
{
_productFolderName = "UtopiaCanteenBackend";
_appDataFolder = null;
_dbPath = null;
}
public static void UseClientStorage()
{
_productFolderName = "UtopiaCanteenClient";
_appDataFolder = null;
_dbPath = null;
}
/// <summary> /// <summary>
/// Folder under LocalApplicationData for DB and config. Created on first use. /// Folder under LocalApplicationData for DB and config. Created on first use.
@ -21,7 +39,7 @@ public static class DatabasePath
_appDataFolder = Path.Combine( _appDataFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"UtopiaCanteenSystem"); _productFolderName);
Directory.CreateDirectory(_appDataFolder); Directory.CreateDirectory(_appDataFolder);
return _appDataFolder; return _appDataFolder;
} }

View File

@ -0,0 +1,181 @@
using System.IO;
using System.Text.Json;
using UtopiaCanteenSystem.Data;
using UtopiaCanteenSystem.Models;
namespace UtopiaCanteenSystem.Services;
/// <summary>
/// Scanner frontend configuration: backend URL, device/site, admin UI. No HRMS/production credentials.
/// </summary>
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<ClientAppConfig>(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; }
}
}

View File

@ -0,0 +1,17 @@
namespace UtopiaCanteenSystem.Services;
/// <summary>
/// Resolves the effective HRMS/UIND MySQL connection: explicit HRMS lookup string when set,
/// otherwise the production <see cref="IConfigService.GetMySqlConnectionString"/> value.
/// </summary>
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();
}
}

View File

@ -218,17 +218,10 @@ public class ConfigService : IConfigService
SaveConfig(); SaveConfig();
} }
//public string GetHrmsLookupConnectionString() => (_config.HrmsLookupConnectionString ?? string.Empty).Trim(); public string GetHrmsLookupConnectionString() =>
ConfigConnectionHelper.GetHrmsOrProductionConnectionString(
public string GetHrmsLookupConnectionString() _config.HrmsLookupConnectionString,
{ _config.MySqlConnectionString);
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) public void SetHrmsLookupConnectionString(string connectionString)
{ {
@ -270,7 +263,9 @@ public class ConfigService : IConfigService
public void SetCentralServerBaseUrl(string url) public void SetCentralServerBaseUrl(string url)
{ {
_config.CentralServerBaseUrl = url?.Trim() ?? string.Empty; var v = url?.Trim() ?? string.Empty;
_config.CentralServerBaseUrl = v;
_config.BackendBaseUrl = v;
SaveConfig(); SaveConfig();
} }
@ -286,6 +281,37 @@ public class ConfigService : IConfigService
SaveConfig(); 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() private AppConfig LoadConfig()
{ {
try try
@ -302,6 +328,8 @@ public class ConfigService : IConfigService
var json = File.ReadAllText(_configPath); var json = File.ReadAllText(_configPath);
var config = JsonSerializer.Deserialize<AppConfig>(json); var config = JsonSerializer.Deserialize<AppConfig>(json);
config ??= new AppConfig(); config ??= new AppConfig();
if (string.IsNullOrWhiteSpace(config.BackendBaseUrl) && !string.IsNullOrWhiteSpace(config.CentralServerBaseUrl))
config.BackendBaseUrl = config.CentralServerBaseUrl;
MigrateScanIntervalFromSecondsIfNeeded(config); MigrateScanIntervalFromSecondsIfNeeded(config);
var changed = EnsureConnectionStringDefaults(config); var changed = EnsureConnectionStringDefaults(config);
if (changed) if (changed)
@ -450,10 +478,13 @@ public class ConfigService : IConfigService
/// <summary>Server (default) or Client.</summary> /// <summary>Server (default) or Client.</summary>
public string AppMode { get; set; } = "Server"; public string AppMode { get; set; } = "Server";
/// <summary>Scanner PCs: base URL of central app API (e.g. http://192.168.1.10:5000).</summary> /// <summary>Legacy name; same as BackendBaseUrl for client PCs.</summary>
public string CentralServerBaseUrl { get; set; } = string.Empty; public string CentralServerBaseUrl { get; set; } = string.Empty;
/// <summary>Central PC: Kestrel listen URL(s), e.g. http://0.0.0.0:5000</summary> /// <summary>Client: remote backend URL. Server: optional override (defaults to localhost from ListenUrls).</summary>
public string BackendBaseUrl { get; set; } = string.Empty;
/// <summary>Central PC: API listen URL(s), e.g. http://0.0.0.0:5000</summary>
public string LocalServerListenUrls { get; set; } = "http://0.0.0.0:5000"; public string LocalServerListenUrls { get; set; } = "http://0.0.0.0:5000";
} }
} }

View File

@ -44,7 +44,10 @@ public interface IConfigService
string GetMySqlConnectionString(); string GetMySqlConnectionString();
void SetMySqlConnectionString(string connectionString); void SetMySqlConnectionString(string connectionString);
/// <summary>Connection string for local HRMS MySQL (employee lookup by RFID). Separate from production sync.</summary> /// <summary>
/// MySQL connection for HRMS/UIND (cache sync, employee lookup, lunch_order post).
/// Uses <c>HrmsLookupConnectionString</c> when set; otherwise <see cref="GetMySqlConnectionString"/>.
/// </summary>
string GetHrmsLookupConnectionString(); string GetHrmsLookupConnectionString();
void SetHrmsLookupConnectionString(string connectionString); void SetHrmsLookupConnectionString(string connectionString);
@ -57,10 +60,14 @@ public interface IConfigService
AppMode GetAppMode(); AppMode GetAppMode();
void SetAppMode(AppMode mode); void SetAppMode(AppMode mode);
/// <summary>Client mode: central server API base URL (e.g. http://192.168.1.10:5000).</summary>
string GetCentralServerBaseUrl(); string GetCentralServerBaseUrl();
void SetCentralServerBaseUrl(string url); void SetCentralServerBaseUrl(string url);
/// <summary>Kestrel listen URL(s) when <see cref="AppMode.Server"/> (e.g. http://0.0.0.0:5000).</summary> /// <summary>Backend API base URL for HTTP calls (client: remote server; server: http://localhost:port).</summary>
string GetBackendBaseUrl();
/// <summary>HttpListener bind URL(s) when <see cref="AppMode.Server"/> (e.g. http://0.0.0.0:5000).</summary>
string GetLocalServerListenUrls(); string GetLocalServerListenUrls();
void SetLocalServerListenUrls(string urls); void SetLocalServerListenUrls(string urls);
} }