(feature): edit components improvements, problem+json messages, account edit form init, redeploy certs controller, interfaces review

This commit is contained in:
Maksym Sadovnychyy 2025-11-03 21:28:42 +01:00
parent edacd27aef
commit 494fcc0f9a
36 changed files with 1088 additions and 491 deletions

View File

@ -118,4 +118,23 @@ public class RegistrationCache {
foreach (var host in hostsToRemove) foreach (var host in hostsToRemove)
CachedCerts.Remove(host); CachedCerts.Remove(host);
} }
/// <summary>
///
/// </summary>
/// <returns></returns>
public Dictionary<string, string> GetCertsPemPerHostname() {
var result = new Dictionary<string, string>();
if (CachedCerts == null)
return result;
foreach (var kvp in CachedCerts) {
var hostname = kvp.Key;
var cert = kvp.Value;
if (!string.IsNullOrEmpty(cert.Cert) && !string.IsNullOrEmpty(cert.PrivatePem)) {
result[hostname] = $"{cert.Cert}\n{cert.PrivatePem}";
}
}
return result;
}
} }

View File

@ -9,7 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MaksIT.Core" Version="1.5.1" /> <PackageReference Include="MaksIT.Core" Version="1.5.1" />
<PackageReference Include="MaksIT.Results" Version="1.1.0" /> <PackageReference Include="MaksIT.Results" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />

View File

@ -11,7 +11,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
private readonly IOptions<Configuration> _appSettings; private readonly IOptions<Configuration> _appSettings;
private readonly ILogger<AutoRenewal> _logger; private readonly ILogger<AutoRenewal> _logger;
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
private readonly ICertsInternalService _certsFlowService; private readonly ICertsFlowService _certsFlowService;
public AutoRenewal( public AutoRenewal(
IOptions<Configuration> appSettings, IOptions<Configuration> appSettings,

View File

@ -1,127 +1,71 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MaksIT.LetsEncryptServer.Services; using MaksIT.LetsEncryptServer.Services;
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests; using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
namespace MaksIT.LetsEncryptServer.Controllers { namespace MaksIT.LetsEncryptServer.Controllers {
/// <summary> /// <summary>
/// Certificates flow controller, used for granular testing purposes /// Certificates flow controller, used for granular testing purposes
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/certs")] [Route("api/certs")]
public class CertsFlowController : ControllerBase { public class CertsFlowController : ControllerBase {
private readonly ICertsFlowService _certsFlowService; private readonly ICertsFlowService _certsFlowService;
public CertsFlowController( public CertsFlowController(ICertsFlowService certsFlowService) {
ICertsFlowService certsFlowService
) {
_certsFlowService = certsFlowService; _certsFlowService = certsFlowService;
} }
/// <summary>
/// Initialize certificate flow session
/// </summary>
/// <returns>sessionId</returns>
[HttpPost("configure-client")] [HttpPost("configure-client")]
public async Task<IActionResult> ConfigureClient([FromBody] ConfigureClientRequest requestData) { public async Task<IActionResult> ConfigureClient([FromBody] ConfigureClientRequest requestData) {
var result = await _certsFlowService.ConfigureClientAsync(requestData); var result = await _certsFlowService.ConfigureClientAsync(requestData.IsStaging);
return result.ToActionResult(); return result.ToActionResult();
} }
/// <summary>
/// Retrieves terms of service
/// </summary>
/// <param name="sessionId">Session ID</param>
/// <returns>Terms of service</returns>
[HttpGet("{sessionId}/terms-of-service")] [HttpGet("{sessionId}/terms-of-service")]
public IActionResult TermsOfService(Guid sessionId) { public IActionResult TermsOfService(Guid sessionId) {
var result = _certsFlowService.GetTermsOfService(sessionId); var result = _certsFlowService.GetTermsOfService(sessionId);
return result.ToActionResult(); return result.ToActionResult();
} }
/// <summary>
/// When a new certificate session is created, create or retrieve cache data by accountId
/// </summary>
/// <param name="sessionId">Session ID</param>
/// <param name="accountId">Account ID</param>
/// <param name="requestData">Request data</param>
/// <returns>Account ID</returns>
[HttpPost("{sessionId}/init/{accountId?}")] [HttpPost("{sessionId}/init/{accountId?}")]
public async Task<IActionResult> Init(Guid sessionId, Guid? accountId, [FromBody] InitRequest requestData) { public async Task<IActionResult> Init(Guid sessionId, Guid? accountId, [FromBody] InitRequest requestData) {
var result = await _certsFlowService.InitAsync(sessionId, accountId, requestData); var result = await _certsFlowService.InitAsync(sessionId, accountId, requestData.Description, requestData.Contacts);
return result.ToActionResult(); return result.ToActionResult();
} }
/// <summary>
/// After account initialization, create a new order request
/// </summary>
/// <param name="sessionId">Session ID</param>
/// <param name="requestData">Request data</param>
/// <returns>New order response</returns>
[HttpPost("{sessionId}/order")] [HttpPost("{sessionId}/order")]
public async Task<IActionResult> NewOrder(Guid sessionId, [FromBody] NewOrderRequest requestData) { public async Task<IActionResult> NewOrder(Guid sessionId, [FromBody] NewOrderRequest requestData) {
var result = await _certsFlowService.NewOrderAsync(sessionId, requestData); var result = await _certsFlowService.NewOrderAsync(sessionId, requestData.Hostnames, requestData.ChallengeType);
return result.ToActionResult(); return result.ToActionResult();
} }
/// <summary>
/// Complete challenges for the new order
/// </summary>
/// <param name="sessionId">Session ID</param>
/// <returns>Challenges completion response</returns>
[HttpPost("{sessionId}/complete-challenges")] [HttpPost("{sessionId}/complete-challenges")]
public async Task<IActionResult> CompleteChallenges(Guid sessionId) { public async Task<IActionResult> CompleteChallenges(Guid sessionId) {
var result = await _certsFlowService.CompleteChallengesAsync(sessionId); var result = await _certsFlowService.CompleteChallengesAsync(sessionId);
return result.ToActionResult(); return result.ToActionResult();
} }
/// <summary>
/// Get order status before certificate retrieval
/// </summary>
/// <param name="sessionId">Session ID</param>
/// <param name="requestData">Request data</param>
/// <returns>Order status</returns>
[HttpGet("{sessionId}/order-status")] [HttpGet("{sessionId}/order-status")]
public async Task<IActionResult> GetOrder(Guid sessionId, [FromBody] GetOrderRequest requestData) { public async Task<IActionResult> GetOrder(Guid sessionId, [FromBody] GetOrderRequest requestData) {
var result = await _certsFlowService.GetOrderAsync(sessionId, requestData); var result = await _certsFlowService.GetOrderAsync(sessionId, requestData.Hostnames);
return result.ToActionResult(); return result.ToActionResult();
} }
/// <summary>
/// Download certificates to local cache
/// </summary>
/// <param name="sessionId">Session ID</param>
/// <param name="requestData">Request data</param>
/// <returns>Certificates download response</returns>
[HttpPost("{sessionId}/certificates/download")] [HttpPost("{sessionId}/certificates/download")]
public async Task<IActionResult> GetCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) { public async Task<IActionResult> GetCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
var result = await _certsFlowService.GetCertificatesAsync(sessionId, requestData); var result = await _certsFlowService.GetCertificatesAsync(sessionId, requestData.Hostnames);
return result.ToActionResult(); return result.ToActionResult();
} }
/// <summary> [HttpPost("{accountId}/certificates/apply")]
/// Apply certificates from local cache to remote server public async Task<IActionResult> ApplyCertificates(Guid accountId) {
/// </summary> var result = await _certsFlowService.ApplyCertificatesAsync(accountId);
/// <param name="sessionId">Session ID</param>
/// <param name="requestData">Request data</param>
/// <returns>Certificates application response</returns>
[HttpPost("{sessionId}/certificates/apply")]
public async Task<IActionResult> ApplyCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
var result = await _certsFlowService.ApplyCertificatesAsync(sessionId, requestData);
return result.ToActionResult(); return result.ToActionResult();
} }
/// <summary>
/// Revoke certificates
/// </summary>
/// <param name="sessionId"></param>
/// <param name="requestData"></param>
/// <returns></returns>
[HttpPost("{sessionId}/certificates/revoke")] [HttpPost("{sessionId}/certificates/revoke")]
public async Task<IActionResult> RevokeCertificates(Guid sessionId, [FromBody] RevokeCertificatesRequest requestData) { public async Task<IActionResult> RevokeCertificates(Guid sessionId, [FromBody] RevokeCertificatesRequest requestData) {
var result = await _certsFlowService.RevokeCertificatesAsync(sessionId, requestData); var result = await _certsFlowService.RevokeCertificatesAsync(sessionId, requestData.Hostnames);
return result.ToActionResult(); return result.ToActionResult();
} }
} }

View File

@ -10,7 +10,7 @@ namespace MaksIT.LetsEncryptServer.Controllers;
[Route(".well-known")] [Route(".well-known")]
public class WellKnownController : ControllerBase { public class WellKnownController : ControllerBase {
private readonly ICertsRestChallengeService _certsFlowService; private readonly ICertsFlowService _certsFlowService;
public WellKnownController( public WellKnownController(
IOptions<Configuration> appSettings, IOptions<Configuration> appSettings,

View File

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MaksIT.Results" Version="1.1.0" /> <PackageReference Include="MaksIT.Results" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />

View File

@ -46,7 +46,7 @@ builder.Services.AddMemoryCache();
builder.Services.RegisterLetsEncrypt(appSettings); builder.Services.RegisterLetsEncrypt(appSettings);
builder.Services.AddSingleton<ICacheService, CacheService>(); builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddSingleton<ICertsFlowService, CertsFlowService>(); builder.Services.AddHttpClient<ICertsFlowService, CertsFlowService>();
builder.Services.AddSingleton<IAccountService, AccountService>(); builder.Services.AddSingleton<IAccountService, AccountService>();
builder.Services.AddHttpClient<IAgentService, AgentService>(); builder.Services.AddHttpClient<IAgentService, AgentService>();
builder.Services.AddHostedService<AutoRenewal>(); builder.Services.AddHostedService<AutoRenewal>();

View File

@ -27,7 +27,7 @@ public class AccountService : IAccountService {
private readonly ILogger<CacheService> _logger; private readonly ILogger<CacheService> _logger;
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
private readonly ICertsInternalService _certsFlowService; private readonly ICertsFlowService _certsFlowService;
public AccountService( public AccountService(
ILogger<CacheService> logger, ILogger<CacheService> logger,

View File

@ -86,13 +86,13 @@ public class CacheService : ICacheService, IDisposable {
return Result<RegistrationCache?>.InternalServerError(null, message); return Result<RegistrationCache?>.InternalServerError(null, message);
} }
var cache = JsonSerializer.Deserialize<RegistrationCache>(json); var cache = json.ToObject<RegistrationCache>();
return Result<RegistrationCache?>.Ok(cache); return Result<RegistrationCache?>.Ok(cache);
} }
private async Task<Result> SaveToCacheInternalAsync(Guid accountId, RegistrationCache cache) { private async Task<Result> SaveToCacheInternalAsync(Guid accountId, RegistrationCache cache) {
var cacheFilePath = GetCacheFilePath(accountId); var cacheFilePath = GetCacheFilePath(accountId);
var json = JsonSerializer.Serialize(cache); var json = cache.ToJson();
await File.WriteAllTextAsync(cacheFilePath, json); await File.WriteAllTextAsync(cacheFilePath, json);
_logger.LogInformation($"Cache file saved for account {accountId}"); _logger.LogInformation($"Cache file saved for account {accountId}");
return Result.Ok(); return Result.Ok();
@ -114,7 +114,6 @@ public class CacheService : ICacheService, IDisposable {
public async Task<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId) { public async Task<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId) {
return await _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId)); return await _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
} }
public async Task<Result> SaveToCacheAsync(Guid accountId, RegistrationCache cache) { public async Task<Result> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {

View File

@ -1,52 +1,31 @@
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Services;
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
using MaksIT.Results; using MaksIT.Results;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
using MaksIT.LetsEncrypt.Services;
namespace MaksIT.LetsEncryptServer.Services; namespace MaksIT.LetsEncryptServer.Services;
public interface ICertsCommonService { public interface ICertsFlowService {
Result<string?> GetTermsOfService(Guid sessionId); Result<string?> GetTermsOfService(Guid sessionId);
Task<Result> CompleteChallengesAsync(Guid sessionId); Task<Result> CompleteChallengesAsync(Guid sessionId);
}
public interface ICertsInternalService : ICertsCommonService {
Task<Result<Guid?>> ConfigureClientAsync(bool isStaging); Task<Result<Guid?>> ConfigureClientAsync(bool isStaging);
Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts); Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts);
Task<Result<List<string>?>> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType); Task<Result<List<string>?>> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType);
Task<Result> GetOrderAsync(Guid sessionId, string[] hostnames); Task<Result> GetOrderAsync(Guid sessionId, string[] hostnames);
Task<Result> GetCertificatesAsync(Guid sessionId, string[] hostnames); Task<Result> GetCertificatesAsync(Guid sessionId, string[] hostnames);
Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid sessionId, string[] hostnames); Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid accountId);
Task<Result> RevokeCertificatesAsync(Guid sessionId, string[] hostnames); Task<Result> RevokeCertificatesAsync(Guid sessionId, string[] hostnames);
Task<Result<Guid?>> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames); Task<Result<Guid?>> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames);
Task<Result> FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames); Task<Result> FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames);
}
public interface ICertsRestService : ICertsCommonService {
Task<Result<Guid?>> ConfigureClientAsync(ConfigureClientRequest requestData);
Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData);
Task<Result<List<string>?>> NewOrderAsync(Guid sessionId, NewOrderRequest requestData);
Task<Result> GetOrderAsync(Guid sessionId, GetOrderRequest requestData);
Task<Result> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
Task<Result> RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData);
}
public interface ICertsRestChallengeService {
Result<string?> AcmeChallenge(string fileName); Result<string?> AcmeChallenge(string fileName);
} }
public interface ICertsFlowService
: ICertsInternalService,
ICertsRestService,
ICertsRestChallengeService { }
public class CertsFlowService : ICertsFlowService { public class CertsFlowService : ICertsFlowService {
private readonly Configuration _appSettings; private readonly Configuration _appSettings;
private readonly ILogger<CertsFlowService> _logger; private readonly ILogger<CertsFlowService> _logger;
private readonly HttpClient _httpClient;
private readonly ILetsEncryptService _letsEncryptService; private readonly ILetsEncryptService _letsEncryptService;
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
private readonly IAgentService _agentService; private readonly IAgentService _agentService;
@ -55,42 +34,55 @@ public class CertsFlowService : ICertsFlowService {
public CertsFlowService( public CertsFlowService(
IOptions<Configuration> appSettings, IOptions<Configuration> appSettings,
ILogger<CertsFlowService> logger, ILogger<CertsFlowService> logger,
HttpClient httpClient,
ILetsEncryptService letsEncryptService, ILetsEncryptService letsEncryptService,
ICacheService cashService, ICacheService cashService,
IAgentService agentService IAgentService agentService
) { ) {
_appSettings = appSettings.Value; _appSettings = appSettings.Value;
_logger = logger; _logger = logger;
_httpClient = httpClient;
_letsEncryptService = letsEncryptService; _letsEncryptService = letsEncryptService;
_cacheService = cashService; _cacheService = cashService;
_agentService = agentService; _agentService = agentService;
_acmePath = _appSettings.AcmeFolder; _acmePath = _appSettings.AcmeFolder;
} }
#region Common methods
public Result<string?> GetTermsOfService(Guid sessionId) { public Result<string?> GetTermsOfService(Guid sessionId) {
var result = _letsEncryptService.GetTermsOfServiceUri(sessionId); var result = _letsEncryptService.GetTermsOfServiceUri(sessionId);
return result; if (!result.IsSuccess || result.Value == null)
return result;
var termsOfServiceUrl = result.Value;
try {
var pdfBytesTask = _httpClient.GetByteArrayAsync(termsOfServiceUrl);
pdfBytesTask.Wait();
var pdfBytes = pdfBytesTask.Result;
var base64 = Convert.ToBase64String(pdfBytes);
return Result<string?>.Ok(base64);
}
catch (Exception ex) {
_logger.LogError(ex, "Failed to download or convert Terms of Service PDF");
return Result<string?>.InternalServerError(null, $"Failed to download or convert Terms of Service PDF: {ex.Message}");
}
} }
public async Task<Result> CompleteChallengesAsync(Guid sessionId) { public async Task<Result> CompleteChallengesAsync(Guid sessionId) {
return await _letsEncryptService.CompleteChallenges(sessionId); return await _letsEncryptService.CompleteChallenges(sessionId);
} }
#endregion
#region Internal methods
public async Task<Result<Guid?>> ConfigureClientAsync(bool isStaging) { public async Task<Result<Guid?>> ConfigureClientAsync(bool isStaging) {
var sessionId = Guid.NewGuid(); var sessionId = Guid.NewGuid();
var result = await _letsEncryptService.ConfigureClient(sessionId, isStaging); var result = await _letsEncryptService.ConfigureClient(sessionId, isStaging);
if (!result.IsSuccess) if (!result.IsSuccess)
return result.ToResultOfType<Guid?>(default); return result.ToResultOfType<Guid?>(default);
return Result<Guid?>.Ok(sessionId); return Result<Guid?>.Ok(sessionId);
} }
public async Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts) { public async Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts) {
RegistrationCache? cache = null; RegistrationCache? cache = null;
if (accountId == null) { if (accountId == null) {
accountId = Guid.NewGuid(); accountId = Guid.NewGuid();
} }
@ -103,8 +95,8 @@ public class CertsFlowService : ICertsFlowService {
else { else {
cache = cacheResult.Value; cache = cacheResult.Value;
} }
} }
var result = await _letsEncryptService.Init(sessionId, accountId.Value, description, contacts, cache); var result = await _letsEncryptService.Init(sessionId, accountId.Value, description, contacts, cache);
if (!result.IsSuccess) if (!result.IsSuccess)
return result.ToResultOfType<Guid?>(default); return result.ToResultOfType<Guid?>(default);
@ -151,20 +143,20 @@ public class CertsFlowService : ICertsFlowService {
return await _letsEncryptService.GetOrder(sessionId, hostnames); return await _letsEncryptService.GetOrder(sessionId, hostnames);
} }
public async Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid sessionId, string[] hostnames) { public async Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid accountId) {
var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId); var cacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!cacheResult.IsSuccess || cacheResult.Value?.CachedCerts == null) if (!cacheResult.IsSuccess || cacheResult.Value?.CachedCerts == null)
return cacheResult.ToResultOfType<Dictionary<string, string>?>(_ => null); return cacheResult.ToResultOfType<Dictionary<string, string>?>(_ => null);
var results = new Dictionary<string, string>(); var cache = cacheResult.Value;
foreach (var hostname in hostnames) { var results = cache.GetCertsPemPerHostname();
CertificateCache? cert;
if (cacheResult.Value.TryGetCachedCertificate(hostname, out cert)) {
var content = $"{cert.Cert}\n{cert.PrivatePem}"; if (cache.IsDisabled)
results.Add(hostname, content); return Result<Dictionary<string, string>?>.BadRequest(null, $"Account {accountId} is disabled");
}
} if (cache.IsStaging)
return Result<Dictionary<string, string>?>.UnprocessableEntity(null, $"Found certs for {string.Join(',', results.Keys)} (staging environment)");
var uploadResult = await _agentService.UploadCerts(results); var uploadResult = await _agentService.UploadCerts(results);
if (!uploadResult.IsSuccess) if (!uploadResult.IsSuccess)
@ -224,9 +216,8 @@ public class CertsFlowService : ICertsFlowService {
if (!certsResult.IsSuccess) if (!certsResult.IsSuccess)
return certsResult.ToResultOfType<Guid?>(default); return certsResult.ToResultOfType<Guid?>(default);
// Bypass applying certificates in staging mode
if (!isStaging) { if (!isStaging) {
var applyCertsResult = await ApplyCertificatesAsync(sessionId, hostnames); var applyCertsResult = await ApplyCertificatesAsync(accountId ?? Guid.Empty);
if (!applyCertsResult.IsSuccess) if (!applyCertsResult.IsSuccess)
return applyCertsResult.ToResultOfType<Guid?>(_ => null); return applyCertsResult.ToResultOfType<Guid?>(_ => null);
} }
@ -251,32 +242,7 @@ public class CertsFlowService : ICertsFlowService {
return Result.Ok(); return Result.Ok();
} }
#endregion
#region REST methods
public async Task<Result<Guid?>> ConfigureClientAsync(ConfigureClientRequest requestData) {
return await ConfigureClientAsync(requestData.IsStaging);
}
public async Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData) {
return await InitAsync(sessionId, accountId, requestData.Description, requestData.Contacts);
}
public async Task<Result<List<string>>> NewOrderAsync(Guid sessionId, NewOrderRequest requestData) {
return await NewOrderAsync(sessionId, requestData.Hostnames, requestData.ChallengeType);
}
public async Task<Result> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) {
return await GetCertificatesAsync(sessionId, requestData.Hostnames);
}
public async Task<Result> GetOrderAsync(Guid sessionId, GetOrderRequest requestData) {
return await GetOrderAsync(sessionId, requestData.Hostnames);
}
public async Task<Result<Dictionary<string, string>>> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) =>
await ApplyCertificatesAsync(sessionId, requestData.Hostnames);
public async Task<Result> RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData) =>
await RevokeCertificatesAsync(sessionId, requestData.Hostnames);
#endregion
#region Acme Challenge REST methods
public Result<string?> AcmeChallenge(string fileName) { public Result<string?> AcmeChallenge(string fileName) {
DeleteExporedChallenges(); DeleteExporedChallenges();
@ -295,6 +261,7 @@ public class CertsFlowService : ICertsFlowService {
var creationTime = File.GetCreationTime(file); var creationTime = File.GetCreationTime(file);
var timeDifference = currentDate - creationTime; var timeDifference = currentDate - creationTime;
if (timeDifference.TotalDays > 1) { if (timeDifference.TotalDays > 1) {
File.Delete(file); File.Delete(file);
_logger.LogInformation($"Deleted file: {file}"); _logger.LogInformation($"Deleted file: {file}");
@ -305,5 +272,4 @@ public class CertsFlowService : ICertsFlowService {
} }
} }
} }
#endregion
} }

View File

@ -18,6 +18,7 @@
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-pdf": "^10.2.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.7.1", "react-router-dom": "^7.7.1",
"react-virtualized": "^9.22.6", "react-virtualized": "^9.22.6",
@ -769,6 +770,191 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@napi-rs/canvas": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.81.tgz",
"integrity": "sha512-ReCjd5SYI/UKx/olaQLC4GtN6wUQGjlgHXs1lvUvWGXfBMR3Fxnik3cL+OxKN5ithNdoU0/GlCrdKcQDFh2XKQ==",
"license": "MIT",
"optional": true,
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.81",
"@napi-rs/canvas-darwin-arm64": "0.1.81",
"@napi-rs/canvas-darwin-x64": "0.1.81",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.81",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.81",
"@napi-rs/canvas-linux-arm64-musl": "0.1.81",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.81",
"@napi-rs/canvas-linux-x64-gnu": "0.1.81",
"@napi-rs/canvas-linux-x64-musl": "0.1.81",
"@napi-rs/canvas-win32-x64-msvc": "0.1.81"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.81.tgz",
"integrity": "sha512-78Lz+AUi+MsWupyZjXwpwQrp1QCwncPvRZrdvrROcZ9Gq9grP7LfQZiGdR8LKyHIq3OR18mDP+JESGT15V1nXw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.81.tgz",
"integrity": "sha512-omejuKgHWKDGoh8rsgsyhm/whwxMaryTQjJTd9zD7hiB9/rzcEEJLHnzXWR5ysy4/tTjHaQotE6k2t8eodTLnA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.81.tgz",
"integrity": "sha512-EYfk+co6BElq5DXNH9PBLYDYwc4QsvIVbyrsVHsxVpn4p6Y3/s8MChgC69AGqj3vzZBQ1qx2CRCMtg5cub+XuQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.81.tgz",
"integrity": "sha512-teh6Q74CyAcH31yLNQGR9MtXSFxlZa5CI6vvNUISI14gWIJWrhOwUAOly+KRe1aztWR0FWTVSPxM4p5y+06aow==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.81.tgz",
"integrity": "sha512-AGEopHFYRzJOjxY+2G1RmHPRnuWvO3Qdhq7sIazlSjxb3Z6dZHg7OB/4ZimXaimPjDACm9qWa6t5bn9bhXvkcw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.81.tgz",
"integrity": "sha512-Bj3m1cl4GIhsigkdwOxii4g4Ump3/QhNpx85IgAlCCYXpaly6mcsWpuDYEabfIGWOWhDUNBOndaQUPfWK1czOQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.81.tgz",
"integrity": "sha512-yg/5NkHykVdwPlD3XObwCa/EswkOwLHswJcI9rHrac+znHsmCSj5AMX/RTU9Z9F6lZTwL60JM2Esit33XhAMiw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.81.tgz",
"integrity": "sha512-tPfMpSEBuV5dJSKexO/UZxpOqnYTaNbG8aKa1ek8QsWu+4SJ/foWkaxscra/RUv85vepx6WWDjzBNbNJsTnO0w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.81.tgz",
"integrity": "sha512-1L0xnYgzqn8Baef+inPvY4dKqdmw3KCBoe0NEDgezuBZN7MA5xElwifoG8609uNdrMtJ9J6QZarsslLRVqri7g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.81.tgz",
"integrity": "sha512-57ryVbhm/z7RE9/UVcS7mrLPdlayLesy+9U0Uf6epCoeSGrs99tfieCcgZWFbIgmByQ1AZnNtFI2N6huqDLlWQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1761,7 +1947,6 @@
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@ -1849,7 +2034,6 @@
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0", "@typescript-eslint/types": "8.38.0",
@ -2095,7 +2279,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -2674,6 +2857,15 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -2965,7 +3157,6 @@
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -4502,6 +4693,24 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"node_modules/make-cancellable-promise": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz",
"integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1"
}
},
"node_modules/make-event-props": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz",
"integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -4511,6 +4720,23 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/merge-refs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz",
"integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/merge-refs?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/merge2": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -4866,6 +5092,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/pdfjs-dist": {
"version": "5.4.296",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
"integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=20.16.0 || >=22.3.0"
},
"optionalDependencies": {
"@napi-rs/canvas": "^0.1.80"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -5011,7 +5249,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -5021,7 +5258,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@ -5041,12 +5277,49 @@
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-pdf": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.2.0.tgz",
"integrity": "sha512-zk0DIL31oCh8cuQycM0SJKfwh4Onz0/Nwi6wTOjgtEjWGUY6eM+/vuzvOP3j70qtEULn7m1JtaeGzud1w5fY2Q==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"dequal": "^2.0.3",
"make-cancellable-promise": "^2.0.0",
"make-event-props": "^2.0.0",
"merge-refs": "^2.0.0",
"pdfjs-dist": "5.4.296",
"tiny-invariant": "^1.0.0",
"warning": "^4.0.0"
},
"funding": {
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-pdf/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/react-redux": { "node_modules/react-redux": {
"version": "9.2.0", "version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@ -5125,8 +5398,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@ -5667,8 +5939,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.2", "version": "2.2.2",
@ -5698,6 +5969,12 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.14", "version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@ -5736,7 +6013,6 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -5867,7 +6143,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -5964,7 +6239,6 @@
"integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.6", "fdir": "^6.4.6",
@ -6055,7 +6329,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -6063,6 +6336,15 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -20,6 +20,7 @@
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-pdf": "^10.2.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.7.1", "react-router-dom": "^7.7.1",
"react-virtualized": "^9.22.6", "react-virtualized": "^9.22.6",

View File

@ -9,6 +9,7 @@ import { UserButton } from './components/UserButton'
import { Toast } from './components/Toast' import { Toast } from './components/Toast'
import { UtilitiesPage } from './pages/UtilitiesPage' import { UtilitiesPage } from './pages/UtilitiesPage'
import { RegisterPage } from './pages/RegisterPage' import { RegisterPage } from './pages/RegisterPage'
import { LetsEncryptTermsOfServicePage } from './pages/LetsEncryptTermsOfServicePage'
interface LayoutWrapperProps { interface LayoutWrapperProps {
@ -83,6 +84,12 @@ const AppMap: AppMapType[] = [
routes: ['/utilities'], routes: ['/utilities'],
page: UtilitiesPage, page: UtilitiesPage,
linkArea: [LinkArea.SideMenu] linkArea: [LinkArea.SideMenu]
},
{
title: 'Terms of Service',
routes: ['/terms-of-service'],
page: LetsEncryptTermsOfServicePage,
linkArea: [LinkArea.SideMenu]
} }
// { // {
@ -153,6 +160,15 @@ enum ApiRoutes {
// ACCOUNT_ID_HOSTNAMES = 'GET|/account/{accountId}/hostnames', // ACCOUNT_ID_HOSTNAMES = 'GET|/account/{accountId}/hostnames',
// ACCOUNT_ID_HOSTNAME_ID = 'GET|/account/{accountId}/hostname/{index}', // ACCOUNT_ID_HOSTNAME_ID = 'GET|/account/{accountId}/hostname/{index}',
// Agents
AGENT_TEST = 'GET|/agent/test',
// Certs flow
CERTS_FLOW_CONFIGURE_CLIENT = 'POST|/certs/configure-client',
CERTS_FLOW_TERMS_OF_SERVICE = 'GET|/certs/{sessionId}/terms-of-service',
CERTS_FLOW_CERTIFICATES_APPLY = 'POST|/certs/{accountId}/certificates/apply',
// Secrets // Secrets
generateSecret = 'GET|/secret/generatesecret', generateSecret = 'GET|/secret/generatesecret',

View File

@ -4,6 +4,11 @@ import { ApiRoutes, GetApiRoute } from './AppMap'
import { store } from './redux/store' import { store } from './redux/store'
import { refreshJwt } from './redux/slices/identitySlice' import { refreshJwt } from './redux/slices/identitySlice'
import { hideLoader, showLoader } from './redux/slices/loaderSlice' import { hideLoader, showLoader } from './redux/slices/loaderSlice'
import { addToast } from './components/Toast/addToast'
import { de } from 'zod/v4/locales'
import { deepPatternMatch } from './functions'
import { ProblemDetails, ProblemDetailsProto } from './models/ProblemDetails'
import { add } from 'lodash'
// Create an Axios instance // Create an Axios instance
const axiosInstance = axios.create({ const axiosInstance = axios.create({
@ -73,19 +78,35 @@ axiosInstance.interceptors.response.use(
error => { error => {
// Handle response error // Handle response error
store.dispatch(hideLoader()) store.dispatch(hideLoader())
if (error.response && error.response.status === 401) { if (error.response) {
if (error.response.status === 401) {
// Handle unauthorized error (e.g., redirect to login) // Handle unauthorized error (e.g., redirect to login)
}
else {
const contentType = error.response.headers['content-type']
if (contentType && contentType.includes('application/problem+json')) {
const problem = error.response.data as ProblemDetails
addToast(`${problem.title}: ${problem.detail}`, 'error')
}
}
} }
return Promise.reject(error) return Promise.reject(error)
} }
) )
const getData = async <TResponse>(url: string): Promise<TResponse | undefined> => { /**
* Performs a GET request and returns the response data.
* @param url The endpoint URL.
* @param timeout Optional timeout in milliseconds to override the default.
*/
const getData = async <TResponse>(url: string, timeout?: number): Promise<TResponse | undefined> => {
try { try {
const response = await axiosInstance.get<TResponse>(url, { const response = await axiosInstance.get<TResponse>(url, {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} },
...(timeout ? { timeout } : {})
}) })
return response.data return response.data
} catch { } catch {
@ -94,13 +115,21 @@ const getData = async <TResponse>(url: string): Promise<TResponse | undefined> =
} }
} }
const postData = async <TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | undefined> => { /**
* Performs a POST request with the given data and returns the response data.
* @param url The endpoint URL.
* @param data The request payload.
* @param timeout Optional timeout in milliseconds to override the default.
*/
const postData = async <TRequest, TResponse>(url: string, data?: TRequest, timeout?: number): Promise<TResponse | undefined> => {
try { try {
const response = await axiosInstance.post<TResponse>(url, data, { const response = await axiosInstance.post<TResponse>(url, data, {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} },
...(timeout ? { timeout } : {})
}) })
return response.data return response.data
} catch { } catch {
// Error is already handled by interceptors, so just return undefined // Error is already handled by interceptors, so just return undefined
@ -108,12 +137,19 @@ const postData = async <TRequest, TResponse>(url: string, data: TRequest): Promi
} }
} }
const patchData = async <TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | undefined> => { /**
* Performs a PATCH request with the given data and returns the response data.
* @param url The endpoint URL.
* @param data The request payload.
* @param timeout Optional timeout in milliseconds to override the default.
*/
const patchData = async <TRequest, TResponse>(url: string, data: TRequest, timeout?: number): Promise<TResponse | undefined> => {
try { try {
const response = await axiosInstance.patch<TResponse>(url, data, { const response = await axiosInstance.patch<TResponse>(url, data, {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} },
...(timeout ? { timeout } : {})
}) })
return response.data return response.data
} catch { } catch {
@ -122,12 +158,19 @@ const patchData = async <TRequest, TResponse>(url: string, data: TRequest): Prom
} }
} }
const putData = async <TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | undefined> => { /**
* Performs a PUT request with the given data and returns the response data.
* @param url The endpoint URL.
* @param data The request payload.
* @param timeout Optional timeout in milliseconds to override the default.
*/
const putData = async <TRequest, TResponse>(url: string, data: TRequest, timeout?: number): Promise<TResponse | undefined> => {
try { try {
const response = await axiosInstance.put<TResponse>(url, data, { const response = await axiosInstance.put<TResponse>(url, data, {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} },
...(timeout ? { timeout } : {})
}) })
return response.data return response.data
} catch { } catch {
@ -136,12 +179,18 @@ const putData = async <TRequest, TResponse>(url: string, data: TRequest): Promis
} }
} }
const deleteData = async <TResponse>(url: string): Promise<TResponse | undefined> => { /**
* Performs a DELETE request and returns the response data.
* @param url The endpoint URL.
* @param timeout Optional timeout in milliseconds to override the default.
*/
const deleteData = async <TResponse>(url: string, timeout?: number): Promise<TResponse | undefined> => {
try { try {
const response = await axiosInstance.delete<TResponse>(url, { const response = await axiosInstance.delete<TResponse>(url, {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} },
...(timeout ? { timeout } : {})
}) })
return response.data return response.data
} catch { } catch {

View File

@ -1,8 +1,7 @@
import React from 'react' import { ReactNode } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
interface ConditionalButtonProps { interface CommonButtonProps {
label: string;
colspan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; colspan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
route?: string; route?: string;
buttonHierarchy?: 'primary' | 'secondary' | 'success' | 'error' | 'warning'; buttonHierarchy?: 'primary' | 'secondary' | 'success' | 'error' | 'warning';
@ -10,10 +9,12 @@ interface ConditionalButtonProps {
disabled?: boolean; disabled?: boolean;
} }
const ButtonComponent: React.FC<ConditionalButtonProps> = (props) => { type ButtonComponentProps =
| ({ label: string; children?: never } & CommonButtonProps)
| ({ children: ReactNode; label?: never } & CommonButtonProps);
const ButtonComponent: React.FC<ButtonComponentProps> = (props) => {
const { const {
label,
colspan, colspan,
route, route,
buttonHierarchy, buttonHierarchy,
@ -21,6 +22,9 @@ const ButtonComponent: React.FC<ConditionalButtonProps> = (props) => {
disabled = false disabled = false
} = props } = props
const isChildren = 'children' in props && props.children !== undefined
const content = 'label' in props ? props.label : props.children
const handleClick = (e?: React.MouseEvent) => { const handleClick = (e?: React.MouseEvent) => {
if (disabled) { if (disabled) {
e?.preventDefault() e?.preventDefault()
@ -53,25 +57,27 @@ const ButtonComponent: React.FC<ConditionalButtonProps> = (props) => {
const disabledClass = disabled ? 'opacity-50 cursor-default' : 'cursor-pointer' const disabledClass = disabled ? 'opacity-50 cursor-default' : 'cursor-pointer'
const centeringClass = isChildren ? 'flex justify-center items-center' : 'text-center'
return route return route
? ( ? (
<Link <Link
to={route} to={route}
className={`${buttonClass} px-4 py-2 rounded ${colspan ? `col-span-${colspan}` : 'w-full'} text-center ${disabledClass}`} className={`${buttonClass} px-4 py-2 rounded ${colspan ? `col-span-${colspan}` : 'w-full'} ${centeringClass} ${disabledClass}`}
onClick={handleClick} onClick={handleClick}
tabIndex={disabled ? -1 : undefined} tabIndex={disabled ? -1 : undefined}
aria-disabled={disabled} aria-disabled={disabled}
style={disabled ? { pointerEvents: 'none' } : undefined} style={disabled ? { pointerEvents: 'none' } : undefined}
> >
{label} {content}
</Link> </Link>
) : ( ) : (
<button <button
className={`${buttonClass} px-4 py-2 rounded ${colspan ? `col-span-${colspan}` : 'w-full'} ${disabledClass}`} className={`${buttonClass} px-4 py-2 rounded ${colspan ? `col-span-${colspan}` : 'w-full'} ${centeringClass} ${disabledClass}`}
onClick={handleClick} onClick={handleClick}
disabled={disabled} disabled={disabled}
> >
{label} {content}
</button> </button>
) )
} }

View File

@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { FieldContainer } from './FieldContainer'
interface CheckBoxComponentProps { interface CheckBoxComponentProps {
colspan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; colspan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
@ -36,23 +37,15 @@ const CheckBoxComponent: React.FC<CheckBoxComponentProps> = (props) => {
} }
return ( return (
<div className={`mb-4 col-span-${colspan}`}> <FieldContainer colspan={colspan} label={label} errorText={errorText}>
<label className={'block text-gray-700 text-sm font-bold mb-2'}> <input
<input type={'checkbox'}
type={'checkbox'} checked={value}
checked={value} onChange={handleOnChange}
onChange={handleOnChange} className={`mr-2 leading-tight ${errorText ? 'border-red-500' : ''}`}
className={`mr-2 leading-tight ${errorText ? 'border-red-500' : ''}`} disabled={disabled}
disabled={disabled} />
/> </FieldContainer>
{label}
</label>
{errorText && (
<p className={'text-red-500 text-xs italic mt-2'}>
{errorText}
</p>
)}
</div>
) )
} }

View File

@ -3,6 +3,7 @@ import { parseISO, formatISO, format, getDaysInMonth, addMonths, subMonths } fro
import { ButtonComponent } from './ButtonComponent' import { ButtonComponent } from './ButtonComponent'
import { TextBoxComponent } from './TextBoxComponent' import { TextBoxComponent } from './TextBoxComponent'
import { CircleX } from 'lucide-react' import { CircleX } from 'lucide-react'
import { FieldContainer } from './FieldContainer'
const DISPLAY_FORMAT = 'yyyy-MM-dd HH:mm' const DISPLAY_FORMAT = 'yyyy-MM-dd HH:mm'
@ -119,9 +120,8 @@ const DateTimePickerComponent: FC<DateTimePickerComponentProps> = ({
}, [showDropdown]) }, [showDropdown])
return ( return (
<div className={`relative mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`} ref={dropdownRef}> <FieldContainer colspan={colspan} label={label} errorText={errorText}>
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label> <div className={'relative'} ref={dropdownRef}>
<div className={'relative'}>
<input <input
type={'text'} type={'text'}
value={value ? formatForDisplay(parsedValue!) : ''} value={value ? formatForDisplay(parsedValue!) : ''}
@ -138,54 +138,53 @@ const DateTimePickerComponent: FC<DateTimePickerComponentProps> = ({
<div className={'absolute top-0 bottom-0 right-2 flex items-center gap-1 pointer-events-auto'}> <div className={'absolute top-0 bottom-0 right-2 flex items-center gap-1 pointer-events-auto'}>
{actionButtons()} {actionButtons()}
</div> </div>
</div>
{showDropdown && !readOnly && !disabled && ( {showDropdown && !readOnly && !disabled && (
<div className={'absolute left-0 right-0 bg-white border border-gray-300 rounded mt-1 w-full shadow-lg z-10'}> <div className={'absolute left-0 right-0 bg-white border border-gray-300 rounded mt-1 w-full shadow-lg z-10'}>
<div className={'flex justify-between items-center px-3 py-2'}> <div className={'flex justify-between items-center px-3 py-2'}>
<button onClick={handlePrevMonth} type={'button'}> <button onClick={handlePrevMonth} type={'button'}>
&lt; &lt;
</button> </button>
<span>{format(currentViewDate, 'MMMM yyyy')}</span> <span>{format(currentViewDate, 'MMMM yyyy')}</span>
<button onClick={handleNextMonth} type={'button'}> <button onClick={handleNextMonth} type={'button'}>
&gt; &gt;
</button> </button>
</div>
<div className={'grid grid-cols-7 gap-1 px-3 py-2'}>
{daysArray.map((day) => (
<div
key={day}
onClick={() => handleDayClick(day)}
className={`p-2 cursor-pointer text-center ${
tempDate.getDate() === day &&
tempDate.getMonth() === currentViewDate.getMonth() &&
tempDate.getFullYear() === currentViewDate.getFullYear()
? 'bg-blue-500 text-white rounded'
: 'hover:bg-gray-200 rounded'
}`}
>
{day}
</div>
))}
</div>
<div className={'px-3 py-2'}>
<TextBoxComponent
label={'Time'}
type={'time'}
value={format(tempDate, 'HH:mm')}
onChange={handleTimeChange}
placeholder={'HH:MM'}
readOnly={readOnly}
/>
</div>
<div className={'px-3 py-2 gap-2 flex justify-between'}>
<ButtonComponent label={'Clear'} buttonHierarchy={'secondary'} onClick={handleClear} />
<ButtonComponent label={'Confirm'} buttonHierarchy={'primary'} onClick={handleConfirm} />
</div>
</div> </div>
<div className={'grid grid-cols-7 gap-1 px-3 py-2'}> )}
{daysArray.map((day) => ( </div>
<div </FieldContainer>
key={day}
onClick={() => handleDayClick(day)}
className={`p-2 cursor-pointer text-center ${
tempDate.getDate() === day &&
tempDate.getMonth() === currentViewDate.getMonth() &&
tempDate.getFullYear() === currentViewDate.getFullYear()
? 'bg-blue-500 text-white rounded'
: 'hover:bg-gray-200 rounded'
}`}
>
{day}
</div>
))}
</div>
<div className={'px-3 py-2'}>
<TextBoxComponent
label={'Time'}
type={'time'}
value={format(tempDate, 'HH:mm')}
onChange={handleTimeChange}
placeholder={'HH:MM'}
readOnly={readOnly}
/>
</div>
<div className={'px-3 py-2 gap-2 flex justify-between'}>
<ButtonComponent label={'Clear'} buttonHierarchy={'secondary'} onClick={handleClear} />
<ButtonComponent label={'Confirm'} buttonHierarchy={'primary'} onClick={handleConfirm} />
</div>
</div>
)}
{errorText && <p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>}
</div>
) )
} }

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { FieldContainer } from './FieldContainer'
interface DualListboxComponentProps { interface DualListboxComponentProps {
label?: string; label?: string;
@ -44,10 +45,7 @@ const DualListboxComponent: React.FC<DualListboxComponentProps> = (props) => {
} }
return ( return (
<div className={`col-span-${colspan}`}> <FieldContainer colspan={colspan} label={label} errorText={errorText}>
<label className={'block text-gray-700 text-sm font-bold mb-2'}>
{label}
</label>
<div className={'flex justify-center items-center gap-4 w-full h-full'}> <div className={'flex justify-center items-center gap-4 w-full h-full'}>
<div className={'flex flex-col'}> <div className={'flex flex-col'}>
<h3>{availableItemsLabel}</h3> <h3>{availableItemsLabel}</h3>
@ -92,12 +90,7 @@ const DualListboxComponent: React.FC<DualListboxComponentProps> = (props) => {
</ul> </ul>
</div> </div>
</div> </div>
{errorText && ( </FieldContainer>
<p className={'text-red-500 text-xs italic mt-2'}>
{errorText}
</p>
)}
</div>
) )
} }

View File

@ -0,0 +1,28 @@
import { FC, ReactNode } from 'react'
interface FieldContainerProps {
colspan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
label?: string;
errorText?: string;
children: ReactNode
}
const FieldContainer: FC<FieldContainerProps> = (props) => {
const {
colspan,
label,
errorText,
children
} = props
return <div className={`${colspan ? `col-span-${colspan}` : 'w-full'}`}>
<label className={`block text-gray-700 text-sm font-bold mb-2 ${!label ? 'invisible' : ''}`}>{label || '\u00A0'}</label>
{children}
<p className={`text-red-500 text-xs italic mt-2 ${!errorText ? 'invisible' : ''}`}>{errorText || '\u00A0'}</p>
</div>
}
export {
FieldContainer
}

View File

@ -1,5 +1,6 @@
import React, { useRef, useState } from 'react' import React, { useRef, useState } from 'react'
import { ButtonComponent } from './ButtonComponent' import { ButtonComponent } from './ButtonComponent'
import { TrashIcon } from 'lucide-react'
interface FileUploadComponentProps { interface FileUploadComponentProps {
label?: string label?: string
@ -124,17 +125,18 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
{/* Clear selection button */} {/* Clear selection button */}
<ButtonComponent <ButtonComponent
label={'Clear selection'}
buttonHierarchy={'secondary'} buttonHierarchy={'secondary'}
onClick={handleClear} onClick={handleClear}
disabled={disabled || selectedFiles.length === 0} disabled={disabled || selectedFiles.length === 0}
colspan={1} colspan={1}
/> >
<TrashIcon />
</ButtonComponent>
{/* Select files button */} {/* Select files button */}
<ButtonComponent <ButtonComponent
colspan={2} colspan={2}
label={label} children={label}
buttonHierarchy={'primary'} buttonHierarchy={'primary'}
onClick={handleSelectFiles} onClick={handleSelectFiles}
disabled={disabled} disabled={disabled}

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { FieldContainer } from './FieldContainer'
interface ListboxComponentProps { interface ListboxComponentProps {
label?: string; label?: string;
@ -35,10 +36,7 @@ const ListboxComponent: React.FC<ListboxComponentProps> = (props) => {
} }
return ( return (
<div className={`col-span-${colspan}`}> <FieldContainer colspan={colspan} label={label} errorText={errorText}>
<label className={'block text-gray-700 text-sm font-bold mb-2'}>
{label}
</label>
<div className={'flex flex-col'}> <div className={'flex flex-col'}>
<h3>{itemsLabel}</h3> <h3>{itemsLabel}</h3>
<ul className={'border p-2 w-40 h-64 overflow-auto'}> <ul className={'border p-2 w-40 h-64 overflow-auto'}>
@ -53,12 +51,7 @@ const ListboxComponent: React.FC<ListboxComponentProps> = (props) => {
))} ))}
</ul> </ul>
</div> </div>
{errorText && ( </FieldContainer>
<p className={'text-red-500 text-xs italic mt-2'}>
{errorText}
</p>
)}
</div>
) )
} }

View File

@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { FieldContainer } from './FieldContainer'
interface RadioOption { interface RadioOption {
value: string value: string
@ -45,8 +46,7 @@ const RadioGroupComponent: React.FC<RadioGroupComponentProps> = (props) => {
} }
return ( return (
<div className={`mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}> <FieldContainer colspan={colspan} label={label} errorText={errorText}>
{label && <label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>}
<div className={'flex flex-col'}> <div className={'flex flex-col'}>
{options.map(option => { {options.map(option => {
// Use default cursor (arrow) if disabled or readOnly, else pointer // Use default cursor (arrow) if disabled or readOnly, else pointer
@ -70,10 +70,7 @@ const RadioGroupComponent: React.FC<RadioGroupComponentProps> = (props) => {
) )
})} })}
</div> </div>
{errorText && ( </FieldContainer>
<p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>
)}
</div>
) )
} }

View File

@ -3,6 +3,7 @@ import { ChangeEvent, FC, useRef, useState } from 'react'
import { TrngResponse } from '../../models/TrngResponse' import { TrngResponse } from '../../models/TrngResponse'
import { getData } from '../../axiosConfig' import { getData } from '../../axiosConfig'
import { ApiRoutes, GetApiRoute } from '../../AppMap' import { ApiRoutes, GetApiRoute } from '../../AppMap'
import { FieldContainer } from './FieldContainer'
interface PasswordGeneratorProps { interface PasswordGeneratorProps {
@ -114,9 +115,7 @@ const SecretComponent: FC<PasswordGeneratorProps> = (props) => {
return ( return (
<div className={`mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}> <FieldContainer colspan={colspan} label={label} errorText={errorText}>
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
<div className={'relative'}> <div className={'relative'}>
<input <input
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
@ -138,9 +137,7 @@ const SecretComponent: FC<PasswordGeneratorProps> = (props) => {
{actionButtons()} {actionButtons()}
</div> </div>
</div> </div>
</FieldContainer>
{errorText && <p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>}
</div>
) )
} }

View File

@ -1,6 +1,7 @@
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { CircleX } from 'lucide-react' import { CircleX } from 'lucide-react'
import { ChangeEvent, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ChangeEvent, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FieldContainer } from './FieldContainer'
export interface SelectBoxComponentOption { export interface SelectBoxComponentOption {
value: string | number value: string | number
@ -167,53 +168,53 @@ const SelectBoxComponent: FC<SelectBoxComponentProps> = (props) => {
} }
return ( return (
<div className={`relative mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}> <FieldContainer colspan={colspan} label={label} errorText={errorText}>
{/* Label for the select input */}
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
<div className={'relative'}> <div className={'relative'}>
<input
type={'text'} <div className={'relative'}>
value={filterValue} <input
onChange={handleFilterChange} type={'text'}
placeholder={placeholder} value={filterValue}
className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline onChange={handleFilterChange}
placeholder={placeholder}
className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline
${errorText ? 'border-red-500' : ''} ${errorText ? 'border-red-500' : ''}
${disabled ? 'bg-gray-100 text-gray-500 cursor-default' : 'bg-white'} ${disabled ? 'bg-gray-100 text-gray-500 cursor-default' : 'bg-white'}
${readOnly && !disabled ? 'text-gray-500 cursor-default' : ''}`} ${readOnly && !disabled ? 'text-gray-500 cursor-default' : ''}`}
disabled={readOnly || disabled} disabled={readOnly || disabled}
// Open dropdown when input is focused. // Open dropdown when input is focused.
onFocus={() => { if (!disabled) setShowDropdown(true) }} onFocus={() => { if (!disabled) setShowDropdown(true) }}
// Delay closing dropdown to allow click events on options. // Delay closing dropdown to allow click events on options.
onBlur={() => setTimeout(() => setShowDropdown(false), 200)} onBlur={() => setTimeout(() => setShowDropdown(false), 200)}
/> />
{/* Action Buttons */} {/* Action Buttons */}
<div <div
className={'absolute top-0 bottom-0 right-2 flex items-center gap-1 pointer-events-auto'} className={'absolute top-0 bottom-0 right-2 flex items-center gap-1 pointer-events-auto'}
> >
{actionButtons()} {actionButtons()}
</div>
</div> </div>
{showDropdown && !disabled && (
<div className={'absolute left-0 right-0 bg-white border border-gray-300 rounded mt-1 w-full shadow-lg z-10'}>
{options.length > 0 ? (
options.map((option) => (
<div
key={option.value}
className={'px-4 py-2 cursor-pointer hover:bg-gray-200'}
onMouseDown={() => handleOptionClick(option.value)}
>
{option.label}
</div>
))
) : (
<div className={'px-4 py-2 text-gray-500'}>No options found</div>
)}
</div>
)}
</div> </div>
</FieldContainer>
{showDropdown && !disabled && (
<div className={'absolute left-0 right-0 bg-white border border-gray-300 rounded mt-1 w-full shadow-lg z-10'}>
{options.length > 0 ? (
options.map((option) => (
<div
key={option.value}
className={'px-4 py-2 cursor-pointer hover:bg-gray-200'}
onMouseDown={() => handleOptionClick(option.value)}
>
{option.label}
</div>
))
) : (
<div className={'px-4 py-2 text-gray-500'}>No options found</div>
)}
</div>
)}
{errorText && <p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>}
</div>
) )
} }

View File

@ -1,5 +1,6 @@
import { Eye, EyeOff } from 'lucide-react' import { Eye, EyeOff } from 'lucide-react'
import { ChangeEvent, FC, useEffect, useRef, useState } from 'react' import { ChangeEvent, FC, useEffect, useRef, useState } from 'react'
import { FieldContainer } from './FieldContainer'
interface TextBoxComponentProps { interface TextBoxComponentProps {
label: string label: string
@ -48,7 +49,7 @@ const TextBoxComponent: FC<TextBoxComponentProps> = (props) => {
// Se il type è "textarea", comportamento invariato // Se il type è "textarea", comportamento invariato
if (type === 'textarea') { if (type === 'textarea') {
return ( return (
<div className={`mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}> <div className={`${colspan ? `col-span-${colspan}` : 'w-full'}`}>
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label> <label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
<textarea <textarea
value={value} value={value}
@ -69,9 +70,7 @@ const TextBoxComponent: FC<TextBoxComponentProps> = (props) => {
const hasContent = String(value).length > 0 const hasContent = String(value).length > 0
return ( return (
<div className={`mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}> <FieldContainer colspan={colspan} label={label} errorText={errorText}>
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
{type === 'password' ? ( {type === 'password' ? (
// Wrapper che contiene input e bottone show/hide, ma bottone solo se c'è contenuto // Wrapper che contiene input e bottone show/hide, ma bottone solo se c'è contenuto
<div className={'relative'}> <div className={'relative'}>
@ -111,9 +110,7 @@ const TextBoxComponent: FC<TextBoxComponentProps> = (props) => {
disabled={disabled} disabled={disabled}
/> />
)} )}
</FieldContainer>
{errorText && <p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>}
</div>
) )
} }

View File

@ -1,3 +1,7 @@
import {
FieldContainer,
} from './FieldContainer'
import { ButtonComponent } from './ButtonComponent' import { ButtonComponent } from './ButtonComponent'
import { CheckBoxComponent } from './CheckBoxComponent' import { CheckBoxComponent } from './CheckBoxComponent'
import { TextBoxComponent } from './TextBoxComponent' import { TextBoxComponent } from './TextBoxComponent'
@ -13,6 +17,7 @@ import { FileUploadComponent } from './FileUploadComponent'
export { export {
FieldContainer as EditorWrapper,
ButtonComponent, ButtonComponent,
CheckBoxComponent, CheckBoxComponent,
DateTimePickerComponent, DateTimePickerComponent,

View File

@ -0,0 +1,96 @@
import { FC } from 'react'
import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout'
import { ButtonComponent, CheckBoxComponent, TextBoxComponent } from '../components/editors'
import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse'
import { useFormState } from '../hooks/useFormState'
import { boolean, object, Schema, string } from 'zod'
interface EditAccountFormProps {
description: string
disabled: boolean
}
const RegisterFormProto = (): EditAccountFormProps => ({
description: '',
disabled: false
})
const RegisterFormSchema: Schema<EditAccountFormProps> = object({
description: string(),
disabled: boolean()
})
interface EditAccountProps {
accountId: string
onSubmitted?: (entity: GetAccountResponse) => void
cancelEnabled?: boolean
onCancel?: () => void
}
const EditAccount: FC<EditAccountProps> = (props) => {
const {
accountId,
onSubmitted,
cancelEnabled,
onCancel
} = props
const {
formState,
errors,
formIsValid,
handleInputChange
} = useFormState<EditAccountFormProps>({
initialState: RegisterFormProto(),
validationSchema: RegisterFormSchema
})
const handleSubmit = () => {
// onSubmitted && onSubmitted(updatedEntity)
}
const handleCancel = () => {
onCancel?.()
}
return <FormContainer>
<FormHeader>Edit Account {accountId}</FormHeader>
<FormContent>
<div className={'grid grid-cols-12 gap-4 w-full'}>
<TextBoxComponent
colspan={12}
label={'Account Description'}
value={formState.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder={'Account Description'}
errorText={errors.description}
/>
<CheckBoxComponent
colspan={12}
label={'Disabled'}
value={formState.disabled}
onChange={(e) => handleInputChange('disabled', e.target.checked)}
errorText={errors.disabled}
/>
<h3 className={'col-span-12'}>Contacts:</h3>
</div>
</FormContent>
<FormFooter
rightChildren={
<ButtonComponent label={'Save'} buttonHierarchy={'primary'} onClick={handleSubmit} />
}
leftChildren={
cancelEnabled && <ButtonComponent label={'Cancel'} buttonHierarchy={'secondary'} onClick={handleCancel} />
}
/>
</FormContainer>
}
export {
EditAccount
}

View File

@ -1,32 +1,31 @@
import { FC, useEffect, useState } from 'react' import { FC, useCallback, useEffect, useState } from 'react'
import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout' import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout'
import { ButtonComponent, CheckBoxComponent, RadioGroupComponent, SelectBoxComponent } from '../components/editors' import { ButtonComponent, CheckBoxComponent, RadioGroupComponent, SelectBoxComponent } from '../components/editors'
import { CacheAccount } from '../entities/CacheAccount' import { CacheAccount } from '../entities/CacheAccount'
import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse' import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse'
import { deleteData, getData } from '../axiosConfig' import { deleteData, getData, postData } from '../axiosConfig'
import { ApiRoutes, GetApiRoute } from '../AppMap' import { ApiRoutes, GetApiRoute } from '../AppMap'
import { enumToArr, formatISODateString } from '../functions' import { formatISODateString } from '../functions'
import { ChallengeType } from '../entities/ChallengeType' import { addToast } from '../components/Toast/addToast'
import { Radio } from 'lucide-react' import { Offcanvas } from '../components/Offcanvas'
import { EditAccount } from './EditAccount'
const Home: FC = () => { const Home: FC = () => {
const [rawd, setRawd] = useState<GetAccountResponse[]>([]) const [rawd, setRawd] = useState<GetAccountResponse[]>([])
const [editingAccount, setEditingAccount] = useState<GetAccountResponse | null>( const [accountId, setAccountId] = useState<string | undefined>(undefined)
null
)
useEffect(() => { const loadData = useCallback(() => {
console.log(GetApiRoute(ApiRoutes.ACCOUNTS).route) getData<GetAccountResponse[]>(GetApiRoute(ApiRoutes.ACCOUNTS).route).then((response) => {
getData<GetAccountResponse []>(GetApiRoute(ApiRoutes.ACCOUNTS).route).then((response) => {
if (!response) return if (!response) return
setRawd(response) setRawd(response)
}) })
}, []) }, [])
useEffect(() => {
loadData()
}, [loadData])
const handleAccountUpdate = (updatedAccount: CacheAccount) => { const handleAccountUpdate = (updatedAccount: CacheAccount) => {
// setAccounts( // setAccounts(
// accounts.map((account) => // accounts.map((account) =>
@ -37,118 +36,144 @@ const Home: FC = () => {
// ) // )
} }
const deleteAccount = (accountId: string) => { const handleDeleteAccount = (accountId: string) => {
deleteData<void>( deleteData<void>(
GetApiRoute(ApiRoutes.ACCOUNT_DELETE) GetApiRoute(ApiRoutes.ACCOUNT_DELETE)
.route.replace('{accountId}', accountId) .route.replace('{accountId}', accountId)
).then((result) => { ).then(_ => {
if (!result) return
setRawd(rawd.filter((account) => account.accountId !== accountId)) setRawd(rawd.filter((account) => account.accountId !== accountId))
}) })
} }
return <FormContainer> const handleEditCancel = () => {
<FormHeader>Home</FormHeader> setAccountId(undefined)
<FormContent> }
<div className={'grid grid-cols-12 gap-4 w-full'}>
{rawd.length === 0 ? const handleRedeployCerts = (accountId: string) => {
<div className={'text-center text-gray-600 col-span-12'}> postData<void, { [key: string]: string }>(GetApiRoute(ApiRoutes.CERTS_FLOW_CERTIFICATES_APPLY).route
.replace('{accountId}', accountId)
).then(response => {
if (!response?.message) return
addToast(response?.message, 'info')
})
}
const handleOnSubmitted = (_: GetAccountResponse) => {
setAccountId(undefined)
loadData()
}
return <>
<FormContainer>
<FormHeader>Home</FormHeader>
<FormContent>
<div className={'grid grid-cols-12 gap-4 w-full'}>
{rawd.length === 0 ?
<div className={'text-center text-gray-600 col-span-12'}>
No accounts registered. No accounts registered.
</div> : </div> :
rawd.map((acc) => ( rawd.map((acc) => (
<div key={acc.accountId} className={'bg-white shadow-lg rounded-lg p-6 mb-6 col-span-12'}> <div key={acc.accountId} className={'bg-white shadow-lg rounded-lg p-6 mb-6 col-span-12'}>
<div className={'grid grid-cols-12 gap-4 w-full'}> <div className={'grid grid-cols-12 gap-4 w-full'}>
<h2 className={'col-span-8'}> <h2 className={'col-span-6'}>
Account: {acc.accountId} Account: {acc.accountId}
</h2> </h2>
<ButtonComponent <ButtonComponent
colspan={2} colspan={2}
onClick={() => deleteAccount(acc.accountId)} onClick={() => handleDeleteAccount(acc.accountId)}
label={'Delete'} label={'Delete Account'}
buttonHierarchy={'error'} buttonHierarchy={'error'}
/> />
<ButtonComponent <ButtonComponent
colspan={2} colspan={2}
onClick={() => setEditingAccount(acc)} children={'Redeploy certs'}
label={'Edit'} buttonHierarchy={'success'}
/> onClick={() => handleRedeployCerts(acc.accountId)}
<h3 className={'col-span-12'}> />
<ButtonComponent
colspan={2}
onClick={() => setAccountId(acc.accountId)}
label={'Edit'}
/>
<h3 className={'col-span-12'}>
Description: {acc.description} Description: {acc.description}
</h3> </h3>
<CheckBoxComponent <CheckBoxComponent
colspan={12} colspan={12}
value={acc.isDisabled} value={acc.isDisabled}
label={'Disabled'} label={'Disabled'}
disabled={true} disabled={true}
/> />
<h3 className={'col-span-12'}>Contacts:</h3> <h3 className={'col-span-12'}>Contacts:</h3>
<ul className={'col-span-12'}> <ul className={'col-span-12'}>
{acc.contacts.map((contact) => ( {acc.contacts.map((contact) => (
<li key={contact} className={'pb-2'}> <li key={contact} className={'pb-2'}>
{contact} {contact}
</li> </li>
))} ))}
</ul> </ul>
<RadioGroupComponent <RadioGroupComponent
colspan={12} colspan={12}
label={'LetsEncrypt Environment'} label={'LetsEncrypt Environment'}
options={[ options={[
{ value: 'staging', label: 'Staging' }, { value: 'staging', label: 'Staging' },
{ value: 'production', label: 'Production' } { value: 'production', label: 'Production' }
]} ]}
value={acc.challengeType ? 'staging' : 'production'}
value={acc.challengeType ? 'staging' : 'production'} disabled={true}
disabled={true} />
/> <h3 className={'col-span-12'}>Hostnames:</h3>
<h3 className={'col-span-12'}>Hostnames:</h3> <ul className={'col-span-12'}>
<ul className={'col-span-12'}> {acc.hostnames?.map((hostname) => (
{acc.hostnames?.map((hostname) => ( <li key={hostname.hostname} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
<li key={hostname.hostname} className={'grid grid-cols-12 gap-4 w-full pb-2'}> <span className={'col-span-3'}>{hostname.hostname}</span>
<span className={'col-span-3'}>{hostname.hostname}</span> <span className={'col-span-3'}>Exp: {formatISODateString(hostname.expires)}</span>
<span className={'col-span-3'}>{formatISODateString(hostname.expires)}</span> <span className={'col-span-3'}>
<span className={'col-span-3'}> <span className={`${hostname.isUpcomingExpire
<span className={`${hostname.isUpcomingExpire ? 'bg-yellow-200 text-yellow-800'
? 'bg-yellow-200 text-yellow-800' : 'bg-green-200 text-green-800'}`}>{hostname.isUpcomingExpire
: 'bg-green-200 text-green-800'}`}>{hostname.isUpcomingExpire ? 'Upcoming'
? 'Upcoming' : 'Not Upcoming'}
: 'Not Upcoming'} </span>
</span> </span>
</span> <CheckBoxComponent
colspan={3}
value={hostname.isDisabled}
label={'Disabled'}
disabled={true}
/>
<CheckBoxComponent </li>
colspan={3} ))}
value={hostname.isDisabled} </ul>
label={'Disabled'} <SelectBoxComponent
disabled={true} label={'Environment'}
/> options={[
{ value: 'production', label: 'Production' },
</li> { value: 'staging', label: 'Staging' }
))} ]}
</ul> value={acc.isStaging ? 'staging' : 'production'}
disabled={true}
<SelectBoxComponent />
label={'Environment'} </div>
options={[
{ value: 'production', label: 'Production' },
{ value: 'staging', label: 'Staging' }
]}
value={acc.isStaging ? 'staging' : 'production'}
disabled={true}
/>
</div> </div>
</div> ))}
))} </div>
</FormContent>
<FormFooter />
</FormContainer>
</div> <Offcanvas isOpen={accountId !== undefined}>
{accountId && <EditAccount
accountId={accountId}
cancelEnabled={true}
onSubmitted={handleOnSubmitted}
onCancel={handleEditCancel}
/>}
</Offcanvas>
</>
</FormContent>
<FormFooter />
</FormContainer>
} }
export { Home } export { Home }

View File

@ -0,0 +1,120 @@
import { FC, useEffect, useRef, useState } from 'react'
import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout'
import { ApiRoutes, GetApiRoute } from '../AppMap'
import { getData, postData } from '../axiosConfig'
import { pdfjs, Document, Page } from 'react-pdf'
import 'react-pdf/dist/Page/AnnotationLayer.css'
import 'react-pdf/dist/Page/TextLayer.css'
import type { PDFDocumentProxy } from 'pdfjs-dist'
const LetsEncryptTermsOfService: FC = () => {
const [pdfUrl, setPdfUrl] = useState<string | null>(null)
const [objectUrl, setObjectUrl] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [numPages, setNumPages] = useState<number>()
const containerRef = useRef<HTMLDivElement>(null)
const [containerWidth, setContainerWidth] = useState<number>()
// Set up pdfjs worker
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString()
useEffect(() => {
const handleResize = () => {
if (containerRef.current) {
const { x } = containerRef.current.getBoundingClientRect()
const width = window.innerWidth - x
setContainerWidth(width)
}
}
handleResize()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
useEffect(() => {
setLoading(true)
postData<{ [key: string]: boolean }, string>(GetApiRoute(ApiRoutes.CERTS_FLOW_CONFIGURE_CLIENT).route, {
isStaging: true
})
.then(response => {
if (!response) return
return getData<string>(GetApiRoute(ApiRoutes.CERTS_FLOW_TERMS_OF_SERVICE).route.replace('{sessionId}', response))
})
.then(base64Pdf => {
if (base64Pdf) {
setPdfUrl(base64Pdf)
} else {
setError('Failed to retrieve PDF.')
}
})
.catch(() => setError('Failed to load Terms of Service.'))
.finally(() => setLoading(false))
}, [])
// Convert base64 to Blob and create object URL
useEffect(() => {
if (!pdfUrl) return
// Remove data URL prefix if present
const base64 = pdfUrl.replace(/^data:application\/pdf;base64,/, '')
const byteCharacters = atob(base64)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: 'application/pdf' })
const url = URL.createObjectURL(blob)
setObjectUrl(url)
return () => {
URL.revokeObjectURL(url)
}
}, [pdfUrl])
const handleDocumentLoadSuccess = ({ numPages: nextNumPages }: PDFDocumentProxy): void => {
setNumPages(nextNumPages)
}
return (
<FormContainer>
<FormHeader>Let's Encrypt Terms of Service</FormHeader>
<FormContent>
{loading && <div>Loading Terms of Service...</div>}
{error && <div style={{ color: 'red' }}>{error}</div>}
{objectUrl && (
<div ref={containerRef} className={'w-full overflow-auto'} style={{ minHeight: 600 }}>
<Document file={objectUrl} onLoadSuccess={handleDocumentLoadSuccess}>
{numPages ? (
Array.from(new Array(numPages), (_, index) => (
<div key={`page_container_${index + 1}`} className={'page-container'}>
<Page
key={`page_${index + 1}`}
pageNumber={index + 1}
width={containerWidth && containerWidth > 0 ? containerWidth : 600}
/>
<div className={'page-number w-full text-center text-sm text-gray-500'}>
Page {index + 1} / {numPages}
</div>
</div>
))
) : (
<div>Loading PDF pages...</div>
)}
</Document>
</div>
)}
</FormContent>
<FormFooter />
</FormContainer>
)
}
export { LetsEncryptTermsOfService }

View File

@ -11,6 +11,8 @@ import { enumToArr } from '../functions'
import { PostAccountRequest, PostAccountRequestSchema } from '../models/letsEncryptServer/account/requests/PostAccountRequest' import { PostAccountRequest, PostAccountRequestSchema } from '../models/letsEncryptServer/account/requests/PostAccountRequest'
import { addToast } from '../components/Toast/addToast' import { addToast } from '../components/Toast/addToast'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { PlusIcon, TrashIcon } from 'lucide-react'
import { FieldContainer } from '../components/editors/FieldContainer'
interface RegisterFormProps { interface RegisterFormProps {
@ -90,7 +92,7 @@ const Register: FC<RegisterProps> = () => {
return return
} }
postData<PostAccountRequest, GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_POST).route, request.data) postData<PostAccountRequest, GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_POST).route, request.data, 120000)
.then(response => { .then(response => {
if (!response) return if (!response) return
@ -115,14 +117,16 @@ const Register: FC<RegisterProps> = () => {
{formState.contacts.map((contact) => ( {formState.contacts.map((contact) => (
<li key={contact} className={'grid grid-cols-12 gap-4 w-full pb-2'}> <li key={contact} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
<span className={'col-span-10'}>{contact}</span> <span className={'col-span-10'}>{contact}</span>
<ButtonComponent <FieldContainer colspan={2}>
colspan={2} <ButtonComponent
label={'TRASH'} onClick={() => {
onClick={() => { const updatedContacts = formState.contacts.filter(c => c !== contact)
const updatedContacts = formState.contacts.filter(c => c !== contact) handleInputChange('contacts', updatedContacts)
handleInputChange('contacts', updatedContacts) }}
}} >
/> <TrashIcon />
</ButtonComponent>
</FieldContainer>
</li> </li>
))} ))}
</ul> </ul>
@ -140,19 +144,23 @@ const Register: FC<RegisterProps> = () => {
type={'text'} type={'text'}
errorText={errors.contact} errorText={errors.contact}
/> />
<ButtonComponent <FieldContainer colspan={2}>
colspan={2} <ButtonComponent
label={'PLUS'} onClick={() => {
onClick={() => { handleInputChange('contacts', [...formState.contacts, formState.contact])
handleInputChange('contacts', [...formState.contacts, formState.contact]) handleInputChange('contact', '')
handleInputChange('contact', '') }}
}} disabled={formState.contact.trim() === ''}
disabled={formState.contact.trim() === ''} >
/> <PlusIcon />
</ButtonComponent>
</FieldContainer>
<div className={'col-span-12'}> <div className={'col-span-12'}>
<SelectBoxComponent <SelectBoxComponent
label={'Challenge Type'} label={'Challenge Type'}
options={enumToArr(ChallengeType).map(ct => ({ value: ct.value, label: ct.displayValue }))} options={enumToArr(ChallengeType)
.map(ct => ({ value: ct.value, label: ct.displayValue }))
.filter(ct => ct.value !== ChallengeType.dns01)}
value={formState.challengeType} value={formState.challengeType}
placeholder={'Select Challenge Type'} placeholder={'Select Challenge Type'}
onChange={(e) => handleInputChange('challengeType', e.target.value)} onChange={(e) => handleInputChange('challengeType', e.target.value)}
@ -164,14 +172,16 @@ const Register: FC<RegisterProps> = () => {
{formState.hostnames.map((hostname) => ( {formState.hostnames.map((hostname) => (
<li key={hostname} className={'grid grid-cols-12 gap-4 w-full'}> <li key={hostname} className={'grid grid-cols-12 gap-4 w-full'}>
<span className={'col-span-10'}>{hostname}</span> <span className={'col-span-10'}>{hostname}</span>
<ButtonComponent <FieldContainer colspan={2}>
colspan={2} <ButtonComponent
label={'TRASH'} onClick={() => {
onClick={() => { const updatedHostnames = formState.hostnames.filter(h => h !== hostname)
const updatedHostnames = formState.hostnames.filter(h => h !== hostname) handleInputChange('hostnames', updatedHostnames)
handleInputChange('hostnames', updatedHostnames) }}
}} >
/> <TrashIcon />
</ButtonComponent>
</FieldContainer>
</li> </li>
))} ))}
</ul> </ul>
@ -189,15 +199,17 @@ const Register: FC<RegisterProps> = () => {
type={'text'} type={'text'}
errorText={errors.hostname} errorText={errors.hostname}
/> />
<ButtonComponent <FieldContainer colspan={2}>
colspan={2} <ButtonComponent
label={'PLUS'} onClick={() => {
onClick={() => { handleInputChange('hostnames', [...formState.hostnames, formState.hostname])
handleInputChange('hostnames', [...formState.hostnames, formState.hostname]) handleInputChange('hostname', '')
handleInputChange('hostname', '') }}
}} disabled={formState.hostname.trim() === ''}
disabled={formState.hostname.trim() === ''} >
/> <PlusIcon />
</ButtonComponent>
</FieldContainer>
<RadioGroupComponent <RadioGroupComponent
colspan={12} colspan={12}
label={'LetsEncrypt Environment'} label={'LetsEncrypt Environment'}

View File

@ -1,11 +1,23 @@
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout' import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout'
import { ButtonComponent, FileUploadComponent } from '../components/editors' import { ButtonComponent, DateTimePickerComponent, FileUploadComponent } from '../components/editors'
import { ApiRoutes, GetApiRoute } from '../AppMap'
import { getData } from '../axiosConfig'
import { addToast } from '../components/Toast/addToast'
const Utilities: FC = () => { const Utilities: FC = () => {
const [files, setFiles] = useState<File[]>([]) const [files, setFiles] = useState<File[]>([])
const hadnleTestAgent = () => {
getData(GetApiRoute(ApiRoutes.AGENT_TEST).route)
.then((response) => {
if (!response) return
addToast(response?.message, 'info')
})
}
return <FormContainer> return <FormContainer>
<FormHeader>Utilities</FormHeader> <FormHeader>Utilities</FormHeader>
<FormContent> <FormContent>
@ -13,15 +25,8 @@ const Utilities: FC = () => {
<ButtonComponent <ButtonComponent
colspan={3} colspan={3}
label={'Test agent'} label={'Test agent'}
buttonHierarchy={'primary'} buttonHierarchy={'warning'}
onClick={() => {}} onClick={hadnleTestAgent}
/>
<ButtonComponent
colspan={3}
label={'Download cache files'}
buttonHierarchy={'secondary'}
onClick={() => {}}
/> />
<FileUploadComponent <FileUploadComponent
@ -31,9 +36,20 @@ const Utilities: FC = () => {
onChange={setFiles} onChange={setFiles}
/> />
<span className={'col-span-12'}></span> <ButtonComponent
</div> colspan={3}
children={'Download cache files'}
buttonHierarchy={'secondary'}
onClick={() => {}}
/>
<ButtonComponent
colspan={3}
children={'Destroy cache files'}
buttonHierarchy={'error'}
onClick={() => {}}
/>
</div>
</FormContent> </FormContent>
<FormFooter /> <FormFooter />
</FormContainer> </FormContainer>

View File

@ -0,0 +1,16 @@
const deepPatternMatch = <T extends object>(pattern: T, obj: unknown): boolean => {
if (typeof obj !== 'object' || obj === null) return false
const objKeys = Object.keys(obj as object)
const patternKeys = Object.keys(pattern)
// obj must not have more keys than pattern
if (objKeys.length > patternKeys.length) return false
for (const key of objKeys) {
if (!(key in pattern)) return false
if (typeof (obj as T)[key as keyof T] !== typeof pattern[key as keyof T]) return false
}
return true
}
export {
deepPatternMatch
}

View File

@ -5,7 +5,7 @@ import {
} from './deepDelta' } from './deepDelta'
import { deepEqualArrays, deepEqual } from './deepEqual' import { deepEqualArrays, deepEqual } from './deepEqual'
import { deepMerge } from './deepMerge' import { deepMerge } from './deepMerge'
import { deepPatternMatch } from './deepPatternMatch'
export { export {
@ -14,5 +14,6 @@ export {
deltaHasOperations, deltaHasOperations,
deepEqualArrays, deepEqualArrays,
deepEqual, deepEqual,
deepMerge deepMerge,
deepPatternMatch
} }

View File

@ -9,6 +9,7 @@ import {
deltaHasOperations, deltaHasOperations,
deepEqual, deepEqual,
deepMerge, deepMerge,
deepPatternMatch
} from './deep' } from './deep'
import { import {
@ -39,6 +40,7 @@ export {
deltaHasOperations, deltaHasOperations,
deepEqual, deepEqual,
deepMerge, deepMerge,
deepPatternMatch,
enumToArr, enumToArr,
enumToObj, enumToObj,

View File

@ -0,0 +1,15 @@
export interface ProblemDetails {
status?: number;
title?: string;
detail?: string;
instance?: string;
extensions: { [key: string]: never };
}
export const ProblemDetailsProto = (): ProblemDetails => ({
status: undefined,
title: undefined,
detail: undefined,
instance: undefined,
extensions: {}
})

View File

@ -0,0 +1,7 @@
import { LetsEncryptTermsOfService } from '../forms/LetsEncryptTermsOfService'
const LetsEncryptTermsOfServicePage = () => {
return <LetsEncryptTermsOfService />
}
export { LetsEncryptTermsOfServicePage }