using System.Text.Json;
namespace OpenNest.Data.MaterialLedger;
///
/// The MaterialLedger address and API key for this PC, stored at
/// %APPDATA%\OpenNest\material-ledger.json. The key is written only in the protected form the
/// caller's protector produces (DPAPI on the desktop), never in plain text. Loading never
/// throws; a missing, corrupt or unreadable file means not configured.
///
public sealed class MaterialLedgerSettings
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
};
/// Base URL of MaterialLedger, e.g. http://ledger:5000. Empty means not configured.
public string BaseUrl { get; set; } = "";
/// The plain API key in memory (ML key name Api:Keys:OpenNest).
public string ApiKey { get; set; } = "";
public bool IsConfigured => !string.IsNullOrWhiteSpace(BaseUrl);
/// %APPDATA%\OpenNest\material-ledger.json.
public static string DefaultPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OpenNest", "material-ledger.json");
///
/// Reads the settings, unprotecting the stored key with . A key
/// that cannot be unprotected (another user or PC) loads as empty so the ledger reports
/// unauthorized instead of failing to start.
///
public static MaterialLedgerSettings Load(string path, Func unprotect)
{
ArgumentNullException.ThrowIfNull(unprotect);
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
return new();
try
{
var file = JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions);
if (file == null)
return new();
var settings = new MaterialLedgerSettings { BaseUrl = NormalizeUrl(file.BaseUrl) };
if (!string.IsNullOrEmpty(file.ProtectedApiKey))
{
try
{
settings.ApiKey = unprotect(file.ProtectedApiKey);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
settings.ApiKey = "";
}
}
return settings;
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
{
return new();
}
}
/// Writes camelCase JSON with the key protected by .
public void Save(string path, Func protect)
{
ArgumentNullException.ThrowIfNull(protect);
var json = JsonSerializer.Serialize(new SettingsFile
{
BaseUrl = NormalizeUrl(BaseUrl),
ProtectedApiKey = string.IsNullOrEmpty(ApiKey) ? "" : protect(ApiKey),
}, JsonOptions);
var directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);
File.WriteAllText(path, json);
}
/// Validate and normalize a ledger base URL; blank is valid and means not configured.
public static bool TryNormalizeUrl(string? url, out string normalized)
{
normalized = NormalizeUrl(url);
if (normalized.Length == 0)
return true;
if (Uri.TryCreate(normalized, UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
return true;
normalized = "";
return false;
}
private static string NormalizeUrl(string? url) => (url ?? "").Trim().TrimEnd('/');
private sealed class SettingsFile
{
public string? BaseUrl { get; set; }
public string? ProtectedApiKey { get; set; }
}
}