Adds 2 units and docs for unit 3. nfr-requirements plan
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace SlpModularCms.Modules.Availability.Services;
|
||||
|
||||
public interface IMasterApiKeyProtector
|
||||
{
|
||||
string Protect(string plainApiKey);
|
||||
string? Unprotect(string encryptedApiKey);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace SlpModularCms.Modules.Availability.Services;
|
||||
|
||||
public interface IMasterAvailabilityService
|
||||
{
|
||||
Task<bool> RegisterAsync(string masterUrl, string apiKey);
|
||||
Task<bool> PushStatusAsync(string apiKey, bool isAvailable, string? disableMessage);
|
||||
Task<string?> GetRegisteredUrlAsync(string apiKey);
|
||||
MasterGateStatus GetMasterStatus();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace SlpModularCms.Modules.Availability.Services;
|
||||
|
||||
public class MasterApiKeyProtector : IMasterApiKeyProtector
|
||||
{
|
||||
private readonly IDataProtector _protector;
|
||||
|
||||
public MasterApiKeyProtector(IDataProtectionProvider provider)
|
||||
=> _protector = provider.CreateProtector("SlpModularCms.Availability.MasterApiKey");
|
||||
|
||||
public string Protect(string plainApiKey)
|
||||
=> _protector.Protect(plainApiKey);
|
||||
|
||||
public string? Unprotect(string encryptedApiKey)
|
||||
{
|
||||
try { return _protector.Unprotect(encryptedApiKey); }
|
||||
catch (CryptographicException) { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SlpModularCms.Modules.Availability.Data.Entities;
|
||||
|
||||
namespace SlpModularCms.Modules.Availability.Services;
|
||||
|
||||
public class MasterAvailabilityService : IMasterAvailabilityService
|
||||
{
|
||||
private readonly MasterAvailabilityServiceDependencies _deps;
|
||||
|
||||
private static volatile bool _masterIsAvailable = true;
|
||||
private static volatile string? _masterDisableMessage;
|
||||
|
||||
public MasterAvailabilityService(MasterAvailabilityServiceDependencies deps)
|
||||
=> _deps = deps;
|
||||
|
||||
public MasterGateStatus GetMasterStatus()
|
||||
=> new(_masterIsAvailable, _masterDisableMessage);
|
||||
|
||||
public async Task<bool> RegisterAsync(string masterUrl, string apiKey)
|
||||
{
|
||||
var existing = await _deps.Repository.GetAsync();
|
||||
|
||||
if (existing is null)
|
||||
{
|
||||
var protectedKey = _deps.KeyProtector.Protect(apiKey);
|
||||
var reg = new MasterRegistration
|
||||
{
|
||||
Id = MasterRegistration.SingletonId,
|
||||
MasterUrl = masterUrl,
|
||||
ApiKey = protectedKey,
|
||||
RegisteredAt = DateTimeOffset.UtcNow,
|
||||
LastContactedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
await _deps.Repository.AddAsync(reg);
|
||||
await _deps.Repository.SaveChangesAsync();
|
||||
_deps.Logger.LogInformation("Master registered for the first time at {MasterUrl}", masterUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IsKeyValid(existing, apiKey))
|
||||
{
|
||||
_deps.Logger.LogWarning("Master API key mismatch on {Endpoint}", "POST /api/v1/master/register");
|
||||
return false;
|
||||
}
|
||||
|
||||
existing.MasterUrl = masterUrl;
|
||||
existing.LastContactedAt = DateTimeOffset.UtcNow;
|
||||
_deps.Repository.Update(existing);
|
||||
await _deps.Repository.SaveChangesAsync();
|
||||
_deps.Logger.LogInformation("Master re-registered at {MasterUrl}", masterUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> PushStatusAsync(string apiKey, bool isAvailable, string? disableMessage)
|
||||
{
|
||||
var existing = await _deps.Repository.GetAsync();
|
||||
if (!IsKeyValid(existing, apiKey))
|
||||
{
|
||||
_deps.Logger.LogWarning("Master API key mismatch on {Endpoint}", "POST /api/v1/master/status");
|
||||
return false;
|
||||
}
|
||||
|
||||
_masterIsAvailable = isAvailable;
|
||||
_masterDisableMessage = disableMessage;
|
||||
|
||||
existing!.LastContactedAt = DateTimeOffset.UtcNow;
|
||||
_deps.Repository.Update(existing);
|
||||
await _deps.Repository.SaveChangesAsync();
|
||||
_deps.Logger.LogInformation(
|
||||
"Master status update received: IsAvailable={IsAvailable}, DisableMessage={DisableMessage}",
|
||||
isAvailable, disableMessage);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<string?> GetRegisteredUrlAsync(string apiKey)
|
||||
{
|
||||
var existing = await _deps.Repository.GetAsync();
|
||||
if (!IsKeyValid(existing, apiKey))
|
||||
{
|
||||
_deps.Logger.LogWarning("Master API key mismatch on {Endpoint}", "GET /api/v1/master/registered-url");
|
||||
return null;
|
||||
}
|
||||
|
||||
existing!.LastContactedAt = DateTimeOffset.UtcNow;
|
||||
_deps.Repository.Update(existing);
|
||||
await _deps.Repository.SaveChangesAsync();
|
||||
_deps.Logger.LogDebug("Get-registered-url called");
|
||||
return existing.MasterUrl;
|
||||
}
|
||||
|
||||
private bool IsKeyValid(MasterRegistration? registration, string apiKey)
|
||||
{
|
||||
if (registration is null) return false;
|
||||
var stored = _deps.KeyProtector.Unprotect(registration.ApiKey);
|
||||
return stored is not null && stored == apiKey;
|
||||
}
|
||||
|
||||
internal static void ResetStaticCacheForTest()
|
||||
{
|
||||
_masterIsAvailable = true;
|
||||
_masterDisableMessage = null;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SlpModularCms.Modules.Availability.Repositories;
|
||||
|
||||
namespace SlpModularCms.Modules.Availability.Services;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public record MasterAvailabilityServiceDependencies(
|
||||
IMasterRegistrationRepository Repository,
|
||||
IMasterApiKeyProtector KeyProtector,
|
||||
ILogger<MasterAvailabilityService> Logger
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SlpModularCms.Modules.Availability.Services;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public record MasterGateStatus(bool IsAvailable, string? DisableMessage);
|
||||
Reference in New Issue
Block a user