(feature): backend controllers review init

This commit is contained in:
Maksym Sadovnychyy 2025-11-05 22:08:03 +01:00
parent 5c7bf224c3
commit 7d60b77c62
18 changed files with 234 additions and 63 deletions

View File

@ -27,6 +27,11 @@ public class AccountController : ControllerBase {
#endregion
#region Account
[HttpGet("account/{accountId:guid}")]
public async Task<IActionResult> GetAccount(Guid accountId) {
var result = await _accountService.GetAccountAsync(accountId);
return result.ToActionResult();
}
[HttpPost("account")]
public async Task<IActionResult> PostAccount([FromBody] PostAccountRequest requestData) {

View File

@ -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<IActionResult> 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<IActionResult> PostCaches([FromBody] byte[] zipBytes) {
var result = await _cacheService.UploadCacheZipAsync(zipBytes);
return result.ToActionResult();
}
[HttpGet("cache/{accountId:guid}/download")]
public async Task<IActionResult> 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<IActionResult> PostAccountCache(Guid accountId, [FromBody] byte[] zipBytes) {
var result = await _cacheService.UploadAccountCacheZipAsync(accountId, zipBytes);
return result.ToActionResult();
}
}

View File

@ -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<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId);
Task<Result> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
Task<Result> DeleteFromCacheAsync(Guid accountId);
Task<Result<byte[]>> DownloadCacheZipAsync();
Task<Result<byte[]?>> DownloadAccountCacheZipAsync(Guid accountId);
Task<Result> UploadCacheZipAsync(byte[] zipBytes);
Task<Result> UploadAccountCacheZipAsync(Guid accountId, byte[] zipBytes);
Task<Result> 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<CacheService> logger,
IOptions<Configuration> 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<Result<byte[]>> DownloadCacheZipAsync() {
try {
if (!Directory.Exists(_cacheDirectory)) {
var message = "Cache directory not found.";
_logger.LogWarning(message);
return Result<byte[]>.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<byte[]>.Ok(zipBytes);
}
catch (Exception ex) {
var message = "Error creating or reading cache zip file.";
_logger.LogError(ex, message);
return Result<byte[]>.InternalServerError(null, [message, .. ex.ExtractMessages()]);
}
}
public async Task<Result<byte[]?>> 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<byte[]?>.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<byte[]?>.Ok(zipBytes);
}
catch (Exception ex) {
var message = "Error creating or reading account cache zip file.";
_logger.LogError(ex, message);
return Result<byte[]?>.InternalServerError(null, [message, .. ex.ExtractMessages()]);
}
}
public async Task<Result> 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<Result> 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<Result> 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<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId) {
return await _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
}

View File

@ -1,14 +1,15 @@

using MaksIT.Core.Abstractions.Webapi;
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
public class PatchAccountRequest {
public class PatchAccountRequest : PatchRequestModelBase {
public PatchAction<string>? Description { get; set; }
public string Description { get; set; }
public PatchAction<bool>? IsDisabled { get; set; }
public bool? IsDisabled { get; set; }
public List<PatchAction<string>>? Contacts { get; set; }
public List<string>? Contacts { get; set; }
public List<PatchHostnameRequest>? Hostnames { get; set; }
}

View File

@ -1,9 +1,10 @@

using MaksIT.Core.Abstractions.Webapi;
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
public class PatchHostnameRequest {
public PatchAction<string>? Hostname { get; set; }
public class PatchHostnameRequest : PatchRequestModelBase {
public string? Hostname { get; set; }
public PatchAction<bool>? IsDisabled { get; set; }
public bool? IsDisabled { get; set; }
}

View File

@ -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<ValidationResult> Validate(ValidationContext validationContext) {
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (string.IsNullOrWhiteSpace(Description))
yield return new ValidationResult("Description is required", new[] { nameof(Description) });

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }
}
}

View File

@ -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<ValidationResult> Validate(ValidationContext validationContext) {
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
}

View File

@ -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<ValidationResult> Validate(ValidationContext validationContext) {
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
}

View File

@ -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<ValidationResult> Validate(ValidationContext validationContext) {
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (string.IsNullOrWhiteSpace(Description))
yield return new ValidationResult("Description is required", new[] { nameof(Description) });

View File

@ -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<ValidationResult> Validate(ValidationContext validationContext) {
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });

View File

@ -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<ValidationResult> Validate(ValidationContext validationContext) {
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
}

View File

@ -10,4 +10,8 @@
<Folder Include="LetsEncryptServer\CertsFlow\Responses\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MaksIT.Core" Version="1.5.1" />
</ItemGroup>
</Project>

View File

@ -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<T> {
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
}
}

View File

@ -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
}
}

View File

@ -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: