460 lines
17 KiB
C#
460 lines
17 KiB
C#
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using UtopiaCanteenSystem.Data;
|
|
using UtopiaCanteenSystem.Models;
|
|
|
|
namespace UtopiaCanteenSystem.Services;
|
|
|
|
/// <summary>
|
|
/// JSON-backed configuration (including remember-me credentials) stored under
|
|
/// LocalApplicationData so it survives app updates when deployed from a file server.
|
|
/// </summary>
|
|
public class ConfigService : IConfigService
|
|
{
|
|
private const string DefaultMySqlConnectionString =
|
|
"SERVER=utopia-industries-rr.c5qech8o9lgg.us-east-1.rds.amazonaws.com;DATABASE=hrms;UID=uind_hrms_user;PASSWORD=UINDHRMS01;";
|
|
|
|
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 GetSyncServiceEnabled() => _config.SyncServiceEnabled;
|
|
|
|
public void SetSyncServiceEnabled(bool enabled)
|
|
{
|
|
_config.SyncServiceEnabled = enabled;
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns MySQL connection string for sync. Set via appsettings.json (copy from appsettings.example.json).
|
|
/// No default credentials; returns empty if not configured.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
public DateTime? GetLastEmployeeRfidCacheSyncUtc() => _config.LastEmployeeRfidCacheSyncUtc;
|
|
|
|
public void SetLastEmployeeRfidCacheSyncUtc(DateTime utc)
|
|
{
|
|
_config.LastEmployeeRfidCacheSyncUtc = utc;
|
|
SaveConfig();
|
|
}
|
|
|
|
public DateTime? GetLastMealMenuCacheSyncUtc() => _config.LastMealMenuCacheSyncUtc;
|
|
|
|
public void SetLastMealMenuCacheSyncUtc(DateTime utc)
|
|
{
|
|
_config.LastMealMenuCacheSyncUtc = utc;
|
|
SaveConfig();
|
|
}
|
|
|
|
public AppMode GetAppMode()
|
|
{
|
|
var raw = (_config.AppMode ?? string.Empty).Trim();
|
|
if (raw.Equals("Client", StringComparison.OrdinalIgnoreCase))
|
|
return AppMode.Client;
|
|
return AppMode.Server;
|
|
}
|
|
|
|
public void SetAppMode(AppMode mode)
|
|
{
|
|
_config.AppMode = mode == AppMode.Client ? "Client" : "Server";
|
|
SaveConfig();
|
|
}
|
|
|
|
public string GetCentralServerBaseUrl() => (_config.CentralServerBaseUrl ?? string.Empty).Trim();
|
|
|
|
public void SetCentralServerBaseUrl(string url)
|
|
{
|
|
_config.CentralServerBaseUrl = url?.Trim() ?? string.Empty;
|
|
SaveConfig();
|
|
}
|
|
|
|
public string GetLocalServerListenUrls()
|
|
{
|
|
var u = (_config.LocalServerListenUrls ?? string.Empty).Trim();
|
|
return string.IsNullOrEmpty(u) ? "http://0.0.0.0:5000" : u;
|
|
}
|
|
|
|
public void SetLocalServerListenUrls(string urls)
|
|
{
|
|
_config.LocalServerListenUrls = urls?.Trim() ?? 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();
|
|
EnsureConnectionStringDefaults(defaults);
|
|
SaveConfig(defaults);
|
|
return defaults;
|
|
}
|
|
|
|
var json = File.ReadAllText(_configPath);
|
|
var config = JsonSerializer.Deserialize<AppConfig>(json);
|
|
config ??= new AppConfig();
|
|
MigrateScanIntervalFromSecondsIfNeeded(config);
|
|
var changed = EnsureConnectionStringDefaults(config);
|
|
if (changed)
|
|
SaveConfig(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();
|
|
}
|
|
}
|
|
|
|
private static bool EnsureConnectionStringDefaults(AppConfig config)
|
|
{
|
|
var changed = false;
|
|
if (string.IsNullOrWhiteSpace(config.MySqlConnectionString))
|
|
{
|
|
config.MySqlConnectionString = DefaultMySqlConnectionString;
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// One-time migration: if old ScanTimeoutSeconds is set and new D/H/M are all zero, convert and persist.
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Masks sensitive connection string values for logging.
|
|
/// Shows server and database but hides password.
|
|
/// </summary>
|
|
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 SyncServiceEnabled { get; set; } = true;
|
|
public bool ScannerConnected { get; set; } = false;
|
|
/// <summary>Legacy: migrated to ScanIntervalDays/Hours/Minutes on first load. Kept for JSON deserialization.</summary>
|
|
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;
|
|
|
|
public DateTime? LastEmployeeRfidCacheSyncUtc { get; set; }
|
|
public DateTime? LastMealMenuCacheSyncUtc { get; set; }
|
|
|
|
/// <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>
|
|
public string CentralServerBaseUrl { get; set; } = string.Empty;
|
|
|
|
/// <summary>Central PC: Kestrel listen URL(s), e.g. http://0.0.0.0:5000</summary>
|
|
public string LocalServerListenUrls { get; set; } = "http://0.0.0.0:5000";
|
|
}
|
|
}
|