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
parent
891ddc9175
commit
bf1f8accd1
|
|
@ -10,6 +10,24 @@ public static class DatabasePath
|
|||
{
|
||||
private static string? _appDataFolder;
|
||||
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>
|
||||
/// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AppConfig>(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
|
|||
/// <summary>Server (default) or Client.</summary>
|
||||
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;
|
||||
|
||||
/// <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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@ public interface IConfigService
|
|||
string GetMySqlConnectionString();
|
||||
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();
|
||||
void SetHrmsLookupConnectionString(string connectionString);
|
||||
|
||||
|
|
@ -57,10 +60,14 @@ public interface IConfigService
|
|||
AppMode GetAppMode();
|
||||
void SetAppMode(AppMode mode);
|
||||
|
||||
/// <summary>Client mode: central server API base URL (e.g. http://192.168.1.10:5000).</summary>
|
||||
string GetCentralServerBaseUrl();
|
||||
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();
|
||||
void SetLocalServerListenUrls(string urls);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue