mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(refactor): move locker to core, cache service and controller improvements
This commit is contained in:
parent
5c204e2c1d
commit
2dfc7259fb
55
src/Core/LockManager.cs
Normal file
55
src/Core/LockManager.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
public class LockManager : IDisposable {
|
||||||
|
private readonly SemaphoreSlim _semaphore;
|
||||||
|
|
||||||
|
public LockManager(int initialCount, int maxCount) {
|
||||||
|
_semaphore = new SemaphoreSlim(initialCount, maxCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> ExecuteWithLockAsync<T>(Func<Task<T>> action) {
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
try {
|
||||||
|
return await action();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteWithLockAsync(Func<Task> action) {
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> ExecuteWithLockAsync<T>(Func<T> action) {
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
try {
|
||||||
|
return await Task.Run(action);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteWithLockAsync(Action action) {
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
try {
|
||||||
|
await Task.Run(action);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
_semaphore?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,18 @@ public class CertificateCache {
|
|||||||
public byte[]? Private { get; set; }
|
public byte[]? Private { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class CachedHostname {
|
||||||
|
public string Hostname { get; set; }
|
||||||
|
public DateTime Expires { get; set; }
|
||||||
|
public bool IsUpcomingExpire { get; set; }
|
||||||
|
|
||||||
|
public CachedHostname(string hostname, DateTime expires, bool isUpcomingExpire) {
|
||||||
|
Hostname = hostname;
|
||||||
|
Expires = expires;
|
||||||
|
IsUpcomingExpire = isUpcomingExpire;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class RegistrationCache {
|
public class RegistrationCache {
|
||||||
|
|
||||||
#region Custom Properties
|
#region Custom Properties
|
||||||
@ -53,6 +65,26 @@ public class RegistrationCache {
|
|||||||
return hostsWithUpcomingSslExpiry.ToArray();
|
return hostsWithUpcomingSslExpiry.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public CachedHostname[] GetHosts() {
|
||||||
|
if (CachedCerts == null)
|
||||||
|
return Array.Empty<CachedHostname>();
|
||||||
|
|
||||||
|
var hosts = new List<CachedHostname>();
|
||||||
|
|
||||||
|
foreach (var result in CachedCerts) {
|
||||||
|
var (subject, cachedChert) = result;
|
||||||
|
|
||||||
|
if (cachedChert.Cert != null) {
|
||||||
|
var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cachedChert.Cert));
|
||||||
|
|
||||||
|
hosts.Add(new CachedHostname(subject, cert.NotAfter, (cert.NotAfter - DateTime.UtcNow).TotalDays < 30));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hosts.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns cached certificate. Certs older than 30 days are not returned
|
/// Returns cached certificate. Certs older than 30 days are not returned
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ using MaksIT.Models.LetsEncryptServer.Cache.Requests;
|
|||||||
namespace MaksIT.LetsEncryptServer.Controllers;
|
namespace MaksIT.LetsEncryptServer.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/cache")]
|
||||||
public class CacheController : ControllerBase {
|
public class CacheController : ControllerBase {
|
||||||
private readonly Configuration _appSettings;
|
private readonly Configuration _appSettings;
|
||||||
private readonly ICacheRestService _cacheService;
|
private readonly ICacheRestService _cacheService;
|
||||||
@ -28,27 +28,39 @@ public class CacheController : ControllerBase {
|
|||||||
return result.ToActionResult();
|
return result.ToActionResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPut("account/{accountId:guid}")]
|
||||||
|
public async Task<IActionResult> PutAccount(Guid accountId, [FromBody] PutAccountRequest requestData) {
|
||||||
|
var result = await _cacheService.PutAccountAsync(accountId, requestData);
|
||||||
|
return result.ToActionResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("account/{accountId:guid}")]
|
||||||
|
public async Task<IActionResult> PatchAccount(Guid accountId, [FromBody] PatchAccountRequest requestData) {
|
||||||
|
var result = await _cacheService.PatchAccountAsync(accountId, requestData);
|
||||||
|
return result.ToActionResult();
|
||||||
|
}
|
||||||
|
|
||||||
#region Contacts
|
#region Contacts
|
||||||
|
|
||||||
[HttpGet("{accountId}/contacts")]
|
[HttpGet("account/{accountId:guid}/contacts")]
|
||||||
public async Task<IActionResult> GetContacts(Guid accountId) {
|
public async Task<IActionResult> GetContacts(Guid accountId) {
|
||||||
var result = await _cacheService.GetContactsAsync(accountId);
|
var result = await _cacheService.GetContactsAsync(accountId);
|
||||||
return result.ToActionResult();
|
return result.ToActionResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{accountId}/contacts")]
|
[HttpPut("account/{accountId:guid}/contacts")]
|
||||||
public async Task<IActionResult> PutContacts(Guid accountId, [FromBody] PutContactsRequest requestData) {
|
public async Task<IActionResult> PutContacts(Guid accountId, [FromBody] PutContactsRequest requestData) {
|
||||||
var result = await _cacheService.PutContactsAsync(accountId, requestData);
|
var result = await _cacheService.PutContactsAsync(accountId, requestData);
|
||||||
return result.ToActionResult();
|
return result.ToActionResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{accountId}/contacts")]
|
[HttpPatch("account/{accountId:guid}/contacts")]
|
||||||
public async Task<IActionResult> PatchContacts(Guid accountId, [FromBody] PatchContactRequest requestData) {
|
public async Task<IActionResult> PatchContacts(Guid accountId, [FromBody] PatchContactsRequest requestData) {
|
||||||
var result = await _cacheService.PatchContactsAsync(accountId, requestData);
|
var result = await _cacheService.PatchContactsAsync(accountId, requestData);
|
||||||
return result.ToActionResult();
|
return result.ToActionResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{accountId}/contacts/{index}")]
|
[HttpDelete("account/{accountId:guid}/contacts/{index:int}")]
|
||||||
public async Task<IActionResult> DeleteContact(Guid accountId, int index) {
|
public async Task<IActionResult> DeleteContact(Guid accountId, int index) {
|
||||||
var result = await _cacheService.DeleteContactAsync(accountId, index);
|
var result = await _cacheService.DeleteContactAsync(accountId, index);
|
||||||
return result.ToActionResult();
|
return result.ToActionResult();
|
||||||
@ -57,7 +69,7 @@ public class CacheController : ControllerBase {
|
|||||||
|
|
||||||
#region Hostnames
|
#region Hostnames
|
||||||
|
|
||||||
[HttpGet("{accountId}/hostnames")]
|
[HttpGet("account/{accountId:guid}/hostnames")]
|
||||||
public async Task<IActionResult> GetHostnames(Guid accountId) {
|
public async Task<IActionResult> GetHostnames(Guid accountId) {
|
||||||
var result = await _cacheService.GetHostnames(accountId);
|
var result = await _cacheService.GetHostnames(accountId);
|
||||||
return result.ToActionResult();
|
return result.ToActionResult();
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Text.Json;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
using DomainResults.Common;
|
using DomainResults.Common;
|
||||||
|
|
||||||
using MaksIT.Core.Extensions;
|
using MaksIT.Core.Extensions;
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.Models;
|
using MaksIT.Models;
|
||||||
using MaksIT.Models.LetsEncryptServer.Cache.Requests;
|
using MaksIT.Models.LetsEncryptServer.Cache.Requests;
|
||||||
using MaksIT.Models.LetsEncryptServer.Cache.Responses;
|
|
||||||
using Models.LetsEncryptServer.Cache.Responses;
|
using Models.LetsEncryptServer.Cache.Responses;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services;
|
namespace MaksIT.LetsEncryptServer.Services;
|
||||||
@ -19,289 +17,247 @@ public interface ICacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public interface ICacheRestService {
|
public interface ICacheRestService {
|
||||||
Task<(GetAccountsResponse?, IDomainResult)> GetAccountsAsync();
|
Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync();
|
||||||
Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId);
|
Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId);
|
||||||
|
Task<(GetAccountResponse?, IDomainResult)> PutAccountAsync(Guid accountId, PutAccountRequest requestData);
|
||||||
#region Contacts
|
Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData);
|
||||||
Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId);
|
Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId);
|
||||||
Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData);
|
Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData);
|
||||||
Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactRequest requestData);
|
Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactsRequest requestData);
|
||||||
Task<IDomainResult> DeleteContactAsync(Guid accountId, int index);
|
Task<IDomainResult> DeleteContactAsync(Guid accountId, int index);
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Hostnames
|
|
||||||
Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
|
Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
||||||
|
|
||||||
private readonly ILogger<CacheService> _logger;
|
private readonly ILogger<CacheService> _logger;
|
||||||
private readonly string _cacheDirectory;
|
private readonly string _cacheDirectory;
|
||||||
private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1, 1);
|
private readonly LockManager _lockManager;
|
||||||
|
|
||||||
public CacheService(
|
public CacheService(ILogger<CacheService> logger) {
|
||||||
ILogger<CacheService> logger
|
|
||||||
) {
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_cacheDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cache");
|
_cacheDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cache");
|
||||||
|
_lockManager = new LockManager(1, 1);
|
||||||
|
|
||||||
if (!Directory.Exists(_cacheDirectory)) {
|
if (!Directory.Exists(_cacheDirectory)) {
|
||||||
Directory.CreateDirectory(_cacheDirectory);
|
Directory.CreateDirectory(_cacheDirectory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the cache file path for the given account ID.
|
||||||
|
/// </summary>
|
||||||
private string GetCacheFilePath(Guid accountId) {
|
private string GetCacheFilePath(Guid accountId) {
|
||||||
return Path.Combine(_cacheDirectory, $"{accountId}.json");
|
return Path.Combine(_cacheDirectory, $"{accountId}.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId) {
|
#region Cache Operations
|
||||||
var cacheFilePath = GetCacheFilePath(accountId);
|
|
||||||
|
|
||||||
await _cacheLock.WaitAsync();
|
public Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId) {
|
||||||
|
return _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
|
||||||
try {
|
|
||||||
if (!File.Exists(cacheFilePath)) {
|
|
||||||
var message = $"Cache file not found for account {accountId}";
|
|
||||||
_logger.LogWarning(message);
|
|
||||||
|
|
||||||
return IDomainResult.Failed<RegistrationCache>(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = await File.ReadAllTextAsync(cacheFilePath);
|
|
||||||
if (string.IsNullOrEmpty(json)) {
|
|
||||||
var message = $"Cache file is empty for account {accountId}";
|
|
||||||
_logger.LogWarning(message);
|
|
||||||
|
|
||||||
return IDomainResult.Failed<RegistrationCache>(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
var cache = JsonSerializer.Deserialize<RegistrationCache>(json);
|
|
||||||
return IDomainResult.Success(cache);
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Error reading cache file for account {accountId}";
|
|
||||||
_logger.LogError(ex, message);
|
|
||||||
|
|
||||||
return IDomainResult.Failed<RegistrationCache?>(message);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
_cacheLock.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
|
private async Task<(RegistrationCache?, IDomainResult)> LoadFromCacheInternalAsync(Guid accountId) {
|
||||||
var cacheFilePath = GetCacheFilePath(accountId);
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
await _cacheLock.WaitAsync();
|
|
||||||
|
|
||||||
try {
|
if (!File.Exists(cacheFilePath)) {
|
||||||
var json = JsonSerializer.Serialize(cache);
|
var message = $"Cache file not found for account {accountId}";
|
||||||
await File.WriteAllTextAsync(cacheFilePath, json);
|
_logger.LogWarning(message);
|
||||||
|
return IDomainResult.Failed<RegistrationCache>(message);
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation($"Cache file saved for account {accountId}");
|
var json = await File.ReadAllTextAsync(cacheFilePath);
|
||||||
|
if (string.IsNullOrEmpty(json)) {
|
||||||
|
var message = $"Cache file is empty for account {accountId}";
|
||||||
|
_logger.LogWarning(message);
|
||||||
|
return IDomainResult.Failed<RegistrationCache>(message);
|
||||||
|
}
|
||||||
|
|
||||||
return DomainResult.Success();
|
var cache = JsonSerializer.Deserialize<RegistrationCache>(json);
|
||||||
}
|
return IDomainResult.Success(cache);
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Error writing cache file for account {accountId}";
|
|
||||||
_logger.LogError(ex, message);
|
|
||||||
|
|
||||||
return IDomainResult.Failed(message);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
_cacheLock.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> DeleteFromCacheAsync(Guid accountId) {
|
public Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
|
||||||
var cacheFilePath = GetCacheFilePath(accountId);
|
return _lockManager.ExecuteWithLockAsync(() => SaveToCacheInternalAsync(accountId, cache));
|
||||||
await _cacheLock.WaitAsync();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (File.Exists(cacheFilePath)) {
|
|
||||||
File.Delete(cacheFilePath);
|
|
||||||
_logger.LogInformation($"Cache file deleted for account {accountId}");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_logger.LogWarning($"Cache file not found for account {accountId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return IDomainResult.Success();
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
var message = $"Error deleting cache file for account {accountId}";
|
|
||||||
_logger.LogError(ex, message);
|
|
||||||
|
|
||||||
return IDomainResult.Failed(message);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
_cacheLock.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IDomainResult> SaveToCacheInternalAsync(Guid accountId, RegistrationCache cache) {
|
||||||
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
|
var json = JsonSerializer.Serialize(cache);
|
||||||
|
await File.WriteAllTextAsync(cacheFilePath, json);
|
||||||
|
_logger.LogInformation($"Cache file saved for account {accountId}");
|
||||||
|
return DomainResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IDomainResult> DeleteFromCacheAsync(Guid accountId) {
|
||||||
|
return _lockManager.ExecuteWithLockAsync(() => DeleteFromCacheInternal(accountId));
|
||||||
|
}
|
||||||
|
|
||||||
#region RestService
|
private IDomainResult DeleteFromCacheInternal(Guid accountId) {
|
||||||
public async Task<(GetAccountsResponse?, IDomainResult)> GetAccountsAsync() {
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
await _cacheLock.WaitAsync();
|
if (File.Exists(cacheFilePath)) {
|
||||||
|
File.Delete(cacheFilePath);
|
||||||
|
_logger.LogInformation($"Cache file deleted for account {accountId}");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_logger.LogWarning($"Cache file not found for account {accountId}");
|
||||||
|
}
|
||||||
|
return DomainResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
try {
|
#region Account Operations
|
||||||
|
|
||||||
|
public async Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync() {
|
||||||
|
return await _lockManager.ExecuteWithLockAsync(async () => {
|
||||||
var cacheFiles = Directory.GetFiles(_cacheDirectory);
|
var cacheFiles = Directory.GetFiles(_cacheDirectory);
|
||||||
if (cacheFiles == null)
|
var accountIds = cacheFiles.Select(x => Path.GetFileNameWithoutExtension(x).ToGuid()).ToArray();
|
||||||
return IDomainResult.Success(new GetAccountsResponse {
|
|
||||||
Accounts = Array.Empty<GetAccountResponse>()
|
|
||||||
});
|
|
||||||
|
|
||||||
var accountIds = cacheFiles.Select(x => Path.GetFileNameWithoutExtension(x).ToGuid());
|
|
||||||
|
|
||||||
var accounts = new List<GetAccountResponse>();
|
var accounts = new List<GetAccountResponse>();
|
||||||
foreach (var accountId in accountIds) {
|
|
||||||
var (account, getAccountResult) = await GetAccountAsync(accountId);
|
|
||||||
if(!getAccountResult.IsSuccess || account == null)
|
|
||||||
return (null, getAccountResult);
|
|
||||||
|
|
||||||
|
foreach (var accountId in accountIds) {
|
||||||
|
var (account, result) = await GetAccountAsync(accountId);
|
||||||
|
if (!result.IsSuccess || account == null) {
|
||||||
|
return (null, result);
|
||||||
|
}
|
||||||
accounts.Add(account);
|
accounts.Add(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDomainResult.Success(new GetAccountsResponse {
|
|
||||||
Accounts = accounts.ToArray()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Error listing cache files";
|
|
||||||
_logger.LogError(ex, message);
|
|
||||||
|
|
||||||
return IDomainResult.Failed<GetAccountsResponse?> (message);
|
return IDomainResult.Success(accounts.ToArray());
|
||||||
}
|
});
|
||||||
finally {
|
|
||||||
_cacheLock.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId) {
|
public async Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId) {
|
||||||
|
return await _lockManager.ExecuteWithLockAsync(async () => {
|
||||||
|
var (cache, result) = await LoadFromCacheAsync(accountId);
|
||||||
|
if (!result.IsSuccess || cache == null) {
|
||||||
|
return (null, result);
|
||||||
|
}
|
||||||
|
|
||||||
await _cacheLock.WaitAsync();
|
var response = new GetAccountResponse {
|
||||||
|
|
||||||
try {
|
|
||||||
var (registrationCache, gerRegistrationCacheResult) = await LoadFromCacheAsync(accountId);
|
|
||||||
if (!gerRegistrationCacheResult.IsSuccess || registrationCache == null)
|
|
||||||
return (null, gerRegistrationCacheResult);
|
|
||||||
|
|
||||||
return IDomainResult.Success(new GetAccountResponse {
|
|
||||||
AccountId = accountId,
|
AccountId = accountId,
|
||||||
Description = registrationCache.Description,
|
Description = cache.Description,
|
||||||
Contacts = registrationCache.Contacts,
|
Contacts = cache.Contacts,
|
||||||
Hostnames = registrationCache.GetHostsWithUpcomingSslExpiry()
|
Hostnames = GetHostnamesFromCache(cache).ToArray()
|
||||||
});
|
};
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Error listing cache files";
|
|
||||||
_logger.LogError(ex, message);
|
|
||||||
|
|
||||||
return IDomainResult.Failed<GetAccountResponse?>(message);
|
return IDomainResult.Success(response);
|
||||||
}
|
});
|
||||||
finally {
|
|
||||||
_cacheLock.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(GetAccountResponse?, IDomainResult)> PutAccountAsync(Guid accountId, PutAccountRequest requestData) {
|
||||||
|
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
||||||
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
|
return (null, loadResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.Description = requestData.Description;
|
||||||
|
cache.Contacts = requestData.Contacts;
|
||||||
|
|
||||||
|
var saveResult = await SaveToCacheAsync(accountId, cache);
|
||||||
|
if (!saveResult.IsSuccess) {
|
||||||
|
return (null, saveResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateGetAccountResponse(accountId, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
|
||||||
|
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
||||||
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
|
return (null, loadResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestData.Description != null) {
|
||||||
|
switch (requestData.Description.Op) {
|
||||||
|
case PatchOperation.Replace:
|
||||||
|
cache.Description = requestData.Description.Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestData.Contacts != null && requestData.Contacts.Any()) {
|
||||||
|
var contacts = cache.Contacts?.ToList() ?? new List<string>();
|
||||||
|
foreach (var action in requestData.Contacts) {
|
||||||
|
switch (action.Op)
|
||||||
|
{
|
||||||
|
case PatchOperation.Add:
|
||||||
|
if (action.Value != null) contacts.Add(action.Value);
|
||||||
|
break;
|
||||||
|
case PatchOperation.Replace:
|
||||||
|
if (action.Index != null && action.Index >= 0 && action.Index < contacts.Count)
|
||||||
|
contacts[action.Index.Value] = action.Value;
|
||||||
|
break;
|
||||||
|
case PatchOperation.Remove:
|
||||||
|
if (action.Index != null && action.Index >= 0 && action.Index < contacts.Count)
|
||||||
|
contacts.RemoveAt(action.Index.Value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.Contacts = contacts.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveResult = await SaveToCacheAsync(accountId, cache);
|
||||||
|
if (!saveResult.IsSuccess) {
|
||||||
|
return (null, saveResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateGetAccountResponse(accountId, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Contacts Operations
|
||||||
|
|
||||||
#region Contacts
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves the contacts list for the account.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accountId">The ID of the account.</param>
|
|
||||||
/// <returns>The contacts list and domain result.</returns>
|
|
||||||
public async Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId) {
|
public async Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null)
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
return (null, loadResult);
|
return (null, loadResult);
|
||||||
|
}
|
||||||
|
|
||||||
return IDomainResult.Success(new GetContactsResponse {
|
return IDomainResult.Success(new GetContactsResponse {
|
||||||
Contacts = cache.Contacts ?? Array.Empty<string>()
|
Contacts = cache.Contacts ?? Array.Empty<string>()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds new contacts to the account. This method initializes the contacts list if it is null.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accountId">The ID of the account.</param>
|
|
||||||
/// <param name="requestData">The request containing the contacts to add.</param>
|
|
||||||
/// <returns>The updated account response and domain result.</returns>
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> PostContactAsync(Guid accountId, PostContactsRequest requestData) {
|
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
|
||||||
if (!loadResult.IsSuccess || cache == null)
|
|
||||||
return (null, loadResult);
|
|
||||||
|
|
||||||
var contacts = cache.Contacts?.ToList() ?? new List<string>();
|
|
||||||
|
|
||||||
if (requestData.Contacts != null) {
|
|
||||||
contacts.AddRange(requestData.Contacts);
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.Contacts = contacts.ToArray();
|
|
||||||
var saveResult = await SaveToCacheAsync(accountId, cache);
|
|
||||||
if (!saveResult.IsSuccess)
|
|
||||||
return (null, saveResult);
|
|
||||||
|
|
||||||
return (new GetAccountResponse {
|
|
||||||
AccountId = accountId,
|
|
||||||
Description = cache.Description,
|
|
||||||
Contacts = cache.Contacts,
|
|
||||||
Hostnames = cache.GetHostsWithUpcomingSslExpiry()
|
|
||||||
}, IDomainResult.Success());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Replaces the entire contacts list for the account.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accountId">The ID of the account.</param>
|
|
||||||
/// <param name="requestData">The request containing the new contacts list.</param>
|
|
||||||
/// <returns>The updated account response and domain result.</returns>
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData) {
|
public async Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null)
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
return (null, loadResult);
|
return (null, loadResult);
|
||||||
|
}
|
||||||
|
|
||||||
cache.Contacts = requestData.Contacts;
|
cache.Contacts = requestData.Contacts;
|
||||||
var saveResult = await SaveToCacheAsync(accountId, cache);
|
var saveResult = await SaveToCacheAsync(accountId, cache);
|
||||||
if (!saveResult.IsSuccess)
|
if (!saveResult.IsSuccess) {
|
||||||
return (null, saveResult);
|
return (null, saveResult);
|
||||||
|
}
|
||||||
|
|
||||||
return (new GetAccountResponse {
|
return CreateGetAccountResponse(accountId, cache);
|
||||||
AccountId = accountId,
|
|
||||||
Description = cache.Description,
|
|
||||||
Contacts = cache.Contacts,
|
|
||||||
Hostnames = cache.GetHostsWithUpcomingSslExpiry()
|
|
||||||
}, IDomainResult.Success());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactsRequest requestData) {
|
||||||
/// Partially updates the contacts list for the account. Supports add, replace, and remove operations.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accountId">The ID of the account.</param>
|
|
||||||
/// <param name="requestData">The request containing the patch operations for contacts.</param>
|
|
||||||
/// <returns>The updated account response and domain result.</returns>
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactRequest requestData) {
|
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null)
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
return (null, loadResult);
|
return (null, loadResult);
|
||||||
|
}
|
||||||
|
|
||||||
var contacts = cache.Contacts?.ToList() ?? new List<string>();
|
var contacts = cache.Contacts?.ToList() ?? new List<string>();
|
||||||
|
|
||||||
foreach (var contact in requestData.Contacts) {
|
foreach (var contact in requestData.Contacts) {
|
||||||
switch (contact.Op) {
|
switch (contact.Op) {
|
||||||
case PatchOperation.Add:
|
case PatchOperation.Add:
|
||||||
if (contact.Value != null)
|
if (contact.Value != null) {
|
||||||
contacts.Add(contact.Value);
|
contacts.Add(contact.Value);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case PatchOperation.Replace:
|
case PatchOperation.Replace:
|
||||||
if (contact.Index.HasValue && contact.Index.Value >= 0 && contact.Index.Value < contacts.Count && contact.Value != null)
|
if (contact.Index.HasValue && contact.Index.Value >= 0 && contact.Index.Value < contacts.Count && contact.Value != null) {
|
||||||
contacts[contact.Index.Value] = contact.Value;
|
contacts[contact.Index.Value] = contact.Value;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case PatchOperation.Remove:
|
case PatchOperation.Remove:
|
||||||
if (contact.Index.HasValue && contact.Index.Value >= 0 && contact.Index.Value < contacts.Count)
|
if (contact.Index.HasValue && contact.Index.Value >= 0 && contact.Index.Value < contacts.Count) {
|
||||||
contacts.RemoveAt(contact.Index.Value);
|
contacts.RemoveAt(contact.Index.Value);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return (null, IDomainResult.Failed("Invalid patch operation."));
|
return (null, IDomainResult.Failed("Invalid patch operation."));
|
||||||
@ -310,76 +266,79 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
|
|
||||||
cache.Contacts = contacts.ToArray();
|
cache.Contacts = contacts.ToArray();
|
||||||
var saveResult = await SaveToCacheAsync(accountId, cache);
|
var saveResult = await SaveToCacheAsync(accountId, cache);
|
||||||
if (!saveResult.IsSuccess)
|
if (!saveResult.IsSuccess) {
|
||||||
return (null, saveResult);
|
return (null, saveResult);
|
||||||
|
}
|
||||||
|
|
||||||
return (new GetAccountResponse {
|
return CreateGetAccountResponse(accountId, cache);
|
||||||
AccountId = accountId,
|
|
||||||
Description = cache.Description,
|
|
||||||
Contacts = cache.Contacts,
|
|
||||||
Hostnames = cache.GetHostsWithUpcomingSslExpiry()
|
|
||||||
}, IDomainResult.Success());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes a contact from the account by index.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accountId">The ID of the account.</param>
|
|
||||||
/// <param name="index">The index of the contact to remove.</param>
|
|
||||||
/// <returns>The domain result indicating success or failure.</returns>
|
|
||||||
public async Task<IDomainResult> DeleteContactAsync(Guid accountId, int index) {
|
public async Task<IDomainResult> DeleteContactAsync(Guid accountId, int index) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null)
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
return loadResult;
|
return loadResult;
|
||||||
|
}
|
||||||
|
|
||||||
var contacts = cache.Contacts?.ToList() ?? new List<string>();
|
var contacts = cache.Contacts?.ToList() ?? new List<string>();
|
||||||
|
|
||||||
if (index >= 0 && index < contacts.Count)
|
if (index >= 0 && index < contacts.Count) {
|
||||||
contacts.RemoveAt(index);
|
contacts.RemoveAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
cache.Contacts = contacts.ToArray();
|
cache.Contacts = contacts.ToArray();
|
||||||
var saveResult = await SaveToCacheAsync(accountId, cache);
|
var saveResult = await SaveToCacheAsync(accountId, cache);
|
||||||
if (!saveResult.IsSuccess)
|
if (!saveResult.IsSuccess) {
|
||||||
return saveResult;
|
return saveResult;
|
||||||
|
}
|
||||||
|
|
||||||
return IDomainResult.Success();
|
return IDomainResult.Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Hostnames
|
#region Hostnames Operations
|
||||||
|
|
||||||
public async Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId) {
|
public async Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache?.CachedCerts == null)
|
if (!loadResult.IsSuccess || cache?.CachedCerts == null) {
|
||||||
return (null, loadResult);
|
return (null, loadResult);
|
||||||
|
|
||||||
var hoststWithUpcomingSslExpire = cache.GetHostsWithUpcomingSslExpiry();
|
|
||||||
|
|
||||||
|
|
||||||
var response = new GetHostnamesResponse {
|
|
||||||
Hostnames = new List<HostnameResponse>()
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var result in cache.CachedCerts) {
|
|
||||||
var (subject, cachedChert) = result;
|
|
||||||
|
|
||||||
var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cachedChert.Cert));
|
|
||||||
|
|
||||||
response.Hostnames.Add(new HostnameResponse {
|
|
||||||
Hostname = subject,
|
|
||||||
Expires = cert.NotBefore,
|
|
||||||
IsUpcomingExpire = hoststWithUpcomingSslExpire.Contains(subject)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDomainResult.Success(response);
|
var hostnames = GetHostnamesFromCache(cache);
|
||||||
|
|
||||||
|
return IDomainResult.Success(new GetHostnamesResponse {
|
||||||
|
Hostnames = hostnames
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<HostnameResponse> GetHostnamesFromCache(RegistrationCache cache) {
|
||||||
|
var hosts = cache.GetHosts().Select(x => new HostnameResponse {
|
||||||
|
Hostname = x.Hostname,
|
||||||
|
Expires = x.Expires,
|
||||||
|
IsUpcomingExpire = x.IsUpcomingExpire
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return hosts;
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Methods
|
||||||
|
|
||||||
|
private (GetAccountResponse?, IDomainResult) CreateGetAccountResponse(Guid accountId, RegistrationCache cache) {
|
||||||
|
var hostnames = GetHostnamesFromCache(cache) ?? new List<HostnameResponse>();
|
||||||
|
|
||||||
|
return (new GetAccountResponse {
|
||||||
|
AccountId = accountId,
|
||||||
|
Description = cache.Description,
|
||||||
|
Contacts = cache.Contacts,
|
||||||
|
Hostnames = hostnames.ToArray()
|
||||||
|
}, IDomainResult.Success());
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
_cacheLock?.Dispose();
|
_lockManager?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
|
||||||
|
public class PatchAccountRequest {
|
||||||
|
|
||||||
|
public PatchAction<string>? Description { get; set; }
|
||||||
|
|
||||||
|
public List<PatchAction<string>>? Contacts { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
|
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
|
||||||
|
|
||||||
public class PatchContactRequest {
|
public class PatchContactsRequest {
|
||||||
public List<PatchAction<string>> Contacts { get; set; }
|
public List<PatchAction<string>> Contacts { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
|
||||||
|
public class PutAccountRequest {
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string[] Contacts { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,6 @@ namespace Models.LetsEncryptServer.Cache.Responses {
|
|||||||
|
|
||||||
public string []? Contacts { get; set; }
|
public string []? Contacts { get; set; }
|
||||||
|
|
||||||
public string[]? Hostnames { get; set; }
|
public HostnameResponse[]? Hostnames { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
using Models.LetsEncryptServer.Cache.Responses;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
|
|
||||||
public class GetAccountsResponse {
|
|
||||||
public GetAccountResponse[] Accounts { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,13 +6,6 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Models.LetsEncryptServer.Cache.Responses {
|
namespace Models.LetsEncryptServer.Cache.Responses {
|
||||||
|
|
||||||
public class HostnameResponse {
|
|
||||||
public string Hostname { get; set; }
|
|
||||||
public DateTime Expires { get; set; }
|
|
||||||
public bool IsUpcomingExpire { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class GetHostnamesResponse {
|
public class GetHostnamesResponse {
|
||||||
public List<HostnameResponse> Hostnames { get; set; }
|
public List<HostnameResponse> Hostnames { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Models.LetsEncryptServer.Cache.Responses {
|
||||||
|
public class HostnameResponse {
|
||||||
|
public string Hostname { get; set; }
|
||||||
|
public DateTime Expires { get; set; }
|
||||||
|
public bool IsUpcomingExpire { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user