From 7d60b77c62ae767fde968ce36117ca73941006eb Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Wed, 5 Nov 2025 22:08:03 +0100 Subject: [PATCH] (feature): backend controllers review init --- .../Controllers/AccountController.cs | 5 + .../Controllers/CacheController.cs | 46 ++++++ .../Services/CacheService.cs | 138 +++++++++++++++++- .../Account/Requests/PatchAccountRequest.cs | 11 +- .../Account/Requests/PatchHostnameRequest.cs | 9 +- .../Account/Requests/PostAccountRequest.cs | 7 +- .../Account/Responses/GetAccountResponse.cs | 5 +- .../Account/Responses/GetHostnameResponse.cs | 5 +- .../Requests/ConfigureClientRequest.cs | 5 +- .../Requests/GetCerificatesRequest.cs | 7 +- .../CertsFlow/Requests/GetOrderRequest.cs | 7 +- .../CertsFlow/Requests/InitRequest.cs | 7 +- .../CertsFlow/Requests/NewOrderRequest.cs | 7 +- .../Requests/RevokeCertificatesRequest.cs | 7 +- src/Models/Models.csproj | 4 + src/Models/PatchAction.cs | 13 -- src/Models/PatchOperation.cs | 13 -- src/docker-compose.override.yml | 1 + 18 files changed, 234 insertions(+), 63 deletions(-) create mode 100644 src/LetsEncryptServer/Controllers/CacheController.cs delete mode 100644 src/Models/PatchAction.cs delete mode 100644 src/Models/PatchOperation.cs diff --git a/src/LetsEncryptServer/Controllers/AccountController.cs b/src/LetsEncryptServer/Controllers/AccountController.cs index a9a4784..b2c18d5 100644 --- a/src/LetsEncryptServer/Controllers/AccountController.cs +++ b/src/LetsEncryptServer/Controllers/AccountController.cs @@ -27,6 +27,11 @@ public class AccountController : ControllerBase { #endregion #region Account + [HttpGet("account/{accountId:guid}")] + public async Task GetAccount(Guid accountId) { + var result = await _accountService.GetAccountAsync(accountId); + return result.ToActionResult(); + } [HttpPost("account")] public async Task PostAccount([FromBody] PostAccountRequest requestData) { diff --git a/src/LetsEncryptServer/Controllers/CacheController.cs b/src/LetsEncryptServer/Controllers/CacheController.cs new file mode 100644 index 0000000..cd962f1 --- /dev/null +++ b/src/LetsEncryptServer/Controllers/CacheController.cs @@ -0,0 +1,46 @@ +using MaksIT.LetsEncryptServer.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LetsEncryptServer.Controllers; + +[ApiController] +[Route("api")] +public class CacheController(ICacheService cacheService) : ControllerBase { + private readonly ICacheService _cacheService = cacheService; + + [HttpGet("caches/download")] + public async Task GetCaches() { + var result = await _cacheService.DownloadCacheZipAsync(); + if (!result.IsSuccess || result.Value == null) { + return result.ToActionResult(); + } + + var bytes = result.Value; + + return File(bytes, "application/zip", "caches.zip"); + } + + [HttpPost("caches/upload")] + public async Task PostCaches([FromBody] byte[] zipBytes) { + var result = await _cacheService.UploadCacheZipAsync(zipBytes); + return result.ToActionResult(); + } + + [HttpGet("cache/{accountId:guid}/download")] + public async Task GetCache(Guid accountId) { + var result = await _cacheService.DownloadAccountCacheZipAsync(accountId); + if (!result.IsSuccess || result.Value == null) { + return result.ToActionResult(); + } + + var bytes = result.Value; + + return File(bytes, "application/zip", $"cache-{accountId}.zip"); + } + + [HttpPost("cache/{accountId:guid}/upload")] + public async Task PostAccountCache(Guid accountId, [FromBody] byte[] zipBytes) { + var result = await _cacheService.UploadAccountCacheZipAsync(accountId, zipBytes); + return result.ToActionResult(); + } +} diff --git a/src/LetsEncryptServer/Services/CacheService.cs b/src/LetsEncryptServer/Services/CacheService.cs index 5c60340..a2dcf8a 100644 --- a/src/LetsEncryptServer/Services/CacheService.cs +++ b/src/LetsEncryptServer/Services/CacheService.cs @@ -1,11 +1,11 @@ -using System.Text.Json; - - -using MaksIT.Core.Extensions; +using MaksIT.Core.Extensions; using MaksIT.Core.Threading; using MaksIT.LetsEncrypt.Entities; using MaksIT.Results; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using System.IO.Compression; +using System.Text.Json; namespace MaksIT.LetsEncryptServer.Services; @@ -14,6 +14,12 @@ public interface ICacheService { Task> LoadAccountFromCacheAsync(Guid accountId); Task SaveToCacheAsync(Guid accountId, RegistrationCache cache); Task DeleteFromCacheAsync(Guid accountId); + + Task> DownloadCacheZipAsync(); + Task> DownloadAccountCacheZipAsync(Guid accountId); + Task UploadCacheZipAsync(byte[] zipBytes); + Task UploadAccountCacheZipAsync(Guid accountId, byte[] zipBytes); + Task ClearCacheAsync(); } public class CacheService : ICacheService, IDisposable { @@ -21,6 +27,8 @@ public class CacheService : ICacheService, IDisposable { private readonly string _cacheDirectory; private readonly LockManager _lockManager; + private readonly string tmpDir = "/tmp"; + public CacheService( ILogger logger, IOptions appsettings @@ -112,6 +120,128 @@ public class CacheService : ICacheService, IDisposable { #endregion + + #region + private string GetTempZipPath(string prefix) + { + var zipName = $"{prefix}_{DateTime.UtcNow:yyyyMMddHHmmss}.zip"; + return Path.Combine(tmpDir, zipName); + } + + private void EnsureTempDirAndDeleteFile(string filePath) + { + Directory.CreateDirectory(tmpDir); + if (File.Exists(filePath)) + File.Delete(filePath); + } + + public async Task> DownloadCacheZipAsync() { + try { + if (!Directory.Exists(_cacheDirectory)) { + var message = "Cache directory not found."; + _logger.LogWarning(message); + return Result.NotFound(null, message); + } + + var zipPath = GetTempZipPath("cache"); + EnsureTempDirAndDeleteFile(zipPath); + ZipFile.CreateFromDirectory(_cacheDirectory, zipPath); + var zipBytes = await File.ReadAllBytesAsync(zipPath); + File.Delete(zipPath); + _logger.LogInformation("Cache zipped to {ZipPath}", zipPath); + return Result.Ok(zipBytes); + } + catch (Exception ex) { + var message = "Error creating or reading cache zip file."; + _logger.LogError(ex, message); + return Result.InternalServerError(null, [message, .. ex.ExtractMessages()]); + } + } + + public async Task> DownloadAccountCacheZipAsync(Guid accountId) { + try { + var cacheFilePath = GetCacheFilePath(accountId); + if (!File.Exists(cacheFilePath)) { + var message = $"Cache file not found for account {accountId}."; + _logger.LogWarning(message); + return Result.NotFound(null, message); + } + var zipPath = GetTempZipPath($"account_cache_{accountId}"); + EnsureTempDirAndDeleteFile(zipPath); + using (var zipArchive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) { + zipArchive.CreateEntryFromFile(cacheFilePath, Path.GetFileName(cacheFilePath)); + } + var zipBytes = await File.ReadAllBytesAsync(zipPath); + File.Delete(zipPath); + _logger.LogInformation("Account cache zipped to {ZipPath}", zipPath); + return Result.Ok(zipBytes); + } + catch (Exception ex) { + var message = "Error creating or reading account cache zip file."; + _logger.LogError(ex, message); + return Result.InternalServerError(null, [message, .. ex.ExtractMessages()]); + } + } + + public async Task UploadCacheZipAsync(byte[] zipBytes) { + try { + var zipPath = GetTempZipPath("upload_cache"); + EnsureTempDirAndDeleteFile(zipPath); + await File.WriteAllBytesAsync(zipPath, zipBytes); + ZipFile.ExtractToDirectory(zipPath, _cacheDirectory, true); + File.Delete(zipPath); + _logger.LogInformation("Cache unzipped from {ZipPath}", zipPath); + return Result.Ok(); + } + catch (Exception ex) { + var message = "Error uploading or extracting cache zip file."; + _logger.LogError(ex, message); + return Result.InternalServerError([message, .. ex.ExtractMessages()]); + } + } + + public async Task UploadAccountCacheZipAsync(Guid accountId, byte[] zipBytes) { + try { + var zipPath = GetTempZipPath($"upload_account_cache_{accountId}"); + EnsureTempDirAndDeleteFile(zipPath); + await File.WriteAllBytesAsync(zipPath, zipBytes); + using (var zipArchive = ZipFile.OpenRead(zipPath)) { + foreach (var entry in zipArchive.Entries) { + var destinationPath = Path.Combine(_cacheDirectory, entry.FullName); + entry.ExtractToFile(destinationPath, true); + } + } + File.Delete(zipPath); + _logger.LogInformation("Account cache unzipped from {ZipPath}", zipPath); + return Result.Ok(); + } + catch (Exception ex) { + var message = "Error uploading or extracting account cache zip file."; + _logger.LogError(ex, message); + return Result.InternalServerError([message, .. ex.ExtractMessages()]); + } + } + + public async Task ClearCacheAsync() { + try { + if (Directory.Exists(_cacheDirectory)) { + Directory.Delete(_cacheDirectory, true); + _logger.LogInformation("Cache directory cleared."); + } + else { + _logger.LogWarning("Cache directory not found to clear."); + } + return Result.Ok(); + } + catch (Exception ex) { + var message = "Error clearing cache directory."; + _logger.LogError(ex, message); + return Result.InternalServerError([message, .. ex.ExtractMessages()]); + } + } + + #endregion + public async Task> LoadAccountFromCacheAsync(Guid accountId) { return await _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId)); } diff --git a/src/Models/LetsEncryptServer/Account/Requests/PatchAccountRequest.cs b/src/Models/LetsEncryptServer/Account/Requests/PatchAccountRequest.cs index 4d675f0..4650bd1 100644 --- a/src/Models/LetsEncryptServer/Account/Requests/PatchAccountRequest.cs +++ b/src/Models/LetsEncryptServer/Account/Requests/PatchAccountRequest.cs @@ -1,14 +1,15 @@ - +using MaksIT.Core.Abstractions.Webapi; + namespace MaksIT.Models.LetsEncryptServer.Account.Requests; -public class PatchAccountRequest { +public class PatchAccountRequest : PatchRequestModelBase { - public PatchAction? Description { get; set; } + public string Description { get; set; } - public PatchAction? IsDisabled { get; set; } + public bool? IsDisabled { get; set; } - public List>? Contacts { get; set; } + public List? Contacts { get; set; } public List? Hostnames { get; set; } } diff --git a/src/Models/LetsEncryptServer/Account/Requests/PatchHostnameRequest.cs b/src/Models/LetsEncryptServer/Account/Requests/PatchHostnameRequest.cs index 12e3f0c..3d72f48 100644 --- a/src/Models/LetsEncryptServer/Account/Requests/PatchHostnameRequest.cs +++ b/src/Models/LetsEncryptServer/Account/Requests/PatchHostnameRequest.cs @@ -1,9 +1,10 @@ - +using MaksIT.Core.Abstractions.Webapi; + namespace MaksIT.Models.LetsEncryptServer.Account.Requests; -public class PatchHostnameRequest { - public PatchAction? Hostname { get; set; } +public class PatchHostnameRequest : PatchRequestModelBase { + public string? Hostname { get; set; } - public PatchAction? IsDisabled { get; set; } + public bool? IsDisabled { get; set; } } diff --git a/src/Models/LetsEncryptServer/Account/Requests/PostAccountRequest.cs b/src/Models/LetsEncryptServer/Account/Requests/PostAccountRequest.cs index e373c7f..dd8eb7d 100644 --- a/src/Models/LetsEncryptServer/Account/Requests/PostAccountRequest.cs +++ b/src/Models/LetsEncryptServer/Account/Requests/PostAccountRequest.cs @@ -1,14 +1,15 @@ -using System.ComponentModel.DataAnnotations; +using MaksIT.Core.Abstractions.Webapi; +using System.ComponentModel.DataAnnotations; namespace MaksIT.Models.LetsEncryptServer.Account.Requests; -public class PostAccountRequest : IValidatableObject { +public class PostAccountRequest : RequestModelBase { public required string Description { get; set; } public required string[] Contacts { get; set; } public required string ChallengeType { get; set; } public required string[] Hostnames { get; set; } public required bool IsStaging { get; set; } - public IEnumerable Validate(ValidationContext validationContext) { + public override IEnumerable Validate(ValidationContext validationContext) { if (string.IsNullOrWhiteSpace(Description)) yield return new ValidationResult("Description is required", new[] { nameof(Description) }); diff --git a/src/Models/LetsEncryptServer/Account/Responses/GetAccountResponse.cs b/src/Models/LetsEncryptServer/Account/Responses/GetAccountResponse.cs index 24ee9ab..1c5d5e5 100644 --- a/src/Models/LetsEncryptServer/Account/Responses/GetAccountResponse.cs +++ b/src/Models/LetsEncryptServer/Account/Responses/GetAccountResponse.cs @@ -1,11 +1,12 @@ -using System; +using MaksIT.Core.Abstractions.Webapi; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MaksIT.Models.LetsEncryptServer.Account.Responses { - public class GetAccountResponse { + public class GetAccountResponse : ResponseModelBase { public Guid AccountId { get; set; } public required bool IsDisabled { get; set; } diff --git a/src/Models/LetsEncryptServer/Account/Responses/GetHostnameResponse.cs b/src/Models/LetsEncryptServer/Account/Responses/GetHostnameResponse.cs index de40757..4067c05 100644 --- a/src/Models/LetsEncryptServer/Account/Responses/GetHostnameResponse.cs +++ b/src/Models/LetsEncryptServer/Account/Responses/GetHostnameResponse.cs @@ -1,11 +1,12 @@ -using System; +using MaksIT.Core.Abstractions.Webapi; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MaksIT.Models.LetsEncryptServer.Account.Responses { - public class GetHostnameResponse { + public class GetHostnameResponse : ResponseModelBase { public required string Hostname { get; set; } public DateTime Expires { get; set; } public bool IsUpcomingExpire { get; set; } diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/ConfigureClientRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/ConfigureClientRequest.cs index 752d468..938bcb8 100644 --- a/src/Models/LetsEncryptServer/CertsFlow/Requests/ConfigureClientRequest.cs +++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/ConfigureClientRequest.cs @@ -1,11 +1,12 @@ -using System; +using MaksIT.Core.Abstractions.Webapi; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests { - public class ConfigureClientRequest { + public class ConfigureClientRequest : RequestModelBase { public bool IsStaging { get; set; } } } diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/GetCerificatesRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/GetCerificatesRequest.cs index c5161f3..afb9494 100644 --- a/src/Models/LetsEncryptServer/CertsFlow/Requests/GetCerificatesRequest.cs +++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/GetCerificatesRequest.cs @@ -1,11 +1,12 @@ -using System.ComponentModel.DataAnnotations; +using MaksIT.Core.Abstractions.Webapi; +using System.ComponentModel.DataAnnotations; namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests { - public class GetCertificatesRequest : IValidatableObject { + public class GetCertificatesRequest : RequestModelBase { public required string[] Hostnames { get; set; } - public IEnumerable Validate(ValidationContext validationContext) { + public override IEnumerable Validate(ValidationContext validationContext) { if (Hostnames == null || Hostnames.Length == 0) yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) }); } diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/GetOrderRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/GetOrderRequest.cs index c39acaf..b610987 100644 --- a/src/Models/LetsEncryptServer/CertsFlow/Requests/GetOrderRequest.cs +++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/GetOrderRequest.cs @@ -1,11 +1,12 @@ -using System.ComponentModel.DataAnnotations; +using MaksIT.Core.Abstractions.Webapi; +using System.ComponentModel.DataAnnotations; namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests { - public class GetOrderRequest : IValidatableObject { + public class GetOrderRequest : RequestModelBase { public required string[] Hostnames { get; set; } - public IEnumerable Validate(ValidationContext validationContext) { + public override IEnumerable Validate(ValidationContext validationContext) { if (Hostnames == null || Hostnames.Length == 0) yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) }); } diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/InitRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/InitRequest.cs index 102383e..f85dc53 100644 --- a/src/Models/LetsEncryptServer/CertsFlow/Requests/InitRequest.cs +++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/InitRequest.cs @@ -1,4 +1,5 @@ -using System; +using MaksIT.Core.Abstractions.Webapi; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -6,11 +7,11 @@ using System.Text; using System.Threading.Tasks; namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests { - public class InitRequest: IValidatableObject { + public class InitRequest : RequestModelBase { public required string Description { get; set; } public required string[] Contacts { get; set; } - public IEnumerable Validate(ValidationContext validationContext) { + public override IEnumerable Validate(ValidationContext validationContext) { if (string.IsNullOrWhiteSpace(Description)) yield return new ValidationResult("Description is required", new[] { nameof(Description) }); diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/NewOrderRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/NewOrderRequest.cs index 62e58b9..c6edba4 100644 --- a/src/Models/LetsEncryptServer/CertsFlow/Requests/NewOrderRequest.cs +++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/NewOrderRequest.cs @@ -1,13 +1,14 @@ -using System.ComponentModel.DataAnnotations; +using MaksIT.Core.Abstractions.Webapi; +using System.ComponentModel.DataAnnotations; namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests { - public class NewOrderRequest : IValidatableObject { + public class NewOrderRequest : RequestModelBase { public required string[] Hostnames { get; set; } public required string ChallengeType { get; set; } - public IEnumerable Validate(ValidationContext validationContext) { + public override IEnumerable Validate(ValidationContext validationContext) { if (Hostnames == null || Hostnames.Length == 0) yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) }); diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs index 27c66b2..9bb1d57 100644 --- a/src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs +++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs @@ -1,4 +1,5 @@ -using System; +using MaksIT.Core.Abstractions.Webapi; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -6,11 +7,11 @@ using System.Text; using System.Threading.Tasks; namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests { - public class RevokeCertificatesRequest : IValidatableObject { + public class RevokeCertificatesRequest : RequestModelBase { public required string [] Hostnames { get; set; } - public IEnumerable Validate(ValidationContext validationContext) { + public override IEnumerable Validate(ValidationContext validationContext) { if (Hostnames == null || Hostnames.Length == 0) yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) }); } diff --git a/src/Models/Models.csproj b/src/Models/Models.csproj index b1a738b..ce34962 100644 --- a/src/Models/Models.csproj +++ b/src/Models/Models.csproj @@ -10,4 +10,8 @@ + + + + diff --git a/src/Models/PatchAction.cs b/src/Models/PatchAction.cs deleted file mode 100644 index d4f6cfc..0000000 --- a/src/Models/PatchAction.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MaksIT.Models { - public class PatchAction { - public PatchOperation Op { get; set; } // Enum for operation type - public int? Index { get; set; } // Index for the operation (for arrays/lists) - public T? Value { get; set; } // Value for the operation - } -} diff --git a/src/Models/PatchOperation.cs b/src/Models/PatchOperation.cs deleted file mode 100644 index f899a54..0000000 --- a/src/Models/PatchOperation.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MaksIT.Models { - public enum PatchOperation { - Add, - Remove, - Replace - } -} diff --git a/src/docker-compose.override.yml b/src/docker-compose.override.yml index 1e9fa33..d14fa56 100644 --- a/src/docker-compose.override.yml +++ b/src/docker-compose.override.yml @@ -27,6 +27,7 @@ services: volumes: - D:\Compose\MaksIT.CertsUI\acme:/acme - D:\Compose\MaksIT.CertsUI\cache:/cache + - D:\Compose\MaksIT.CertsUI\tmp:/tmp - D:\Compose\MaksIT.CertsUI\configMap\appsettings.json:/configMap/appsettings.json:ro - D:\Compose\MaksIT.CertsUI\secrets\appsecrets.json:/secrets/appsecrets.json:ro networks: