(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)
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>
<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.DependencyInjection.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 ILogger<AutoRenewal> _logger;
private readonly ICacheService _cacheService;
private readonly ICertsInternalService _certsFlowService;
private readonly ICertsFlowService _certsFlowService;
public AutoRenewal(
IOptions<Configuration> appSettings,

View File

@ -1,127 +1,71 @@
using Microsoft.AspNetCore.Mvc;
using MaksIT.LetsEncryptServer.Services;
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
namespace MaksIT.LetsEncryptServer.Controllers {
/// <summary>
/// Certificates flow controller, used for granular testing purposes
/// </summary>
[ApiController]
[Route("api/certs")]
public class CertsFlowController : ControllerBase {
private readonly ICertsFlowService _certsFlowService;
public CertsFlowController(
ICertsFlowService certsFlowService
) {
public CertsFlowController(ICertsFlowService certsFlowService) {
_certsFlowService = certsFlowService;
}
/// <summary>
/// Initialize certificate flow session
/// </summary>
/// <returns>sessionId</returns>
[HttpPost("configure-client")]
public async Task<IActionResult> ConfigureClient([FromBody] ConfigureClientRequest requestData) {
var result = await _certsFlowService.ConfigureClientAsync(requestData);
var result = await _certsFlowService.ConfigureClientAsync(requestData.IsStaging);
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")]
public IActionResult TermsOfService(Guid sessionId) {
var result = _certsFlowService.GetTermsOfService(sessionId);
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?}")]
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();
}
/// <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")]
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();
}
/// <summary>
/// Complete challenges for the new order
/// </summary>
/// <param name="sessionId">Session ID</param>
/// <returns>Challenges completion response</returns>
[HttpPost("{sessionId}/complete-challenges")]
public async Task<IActionResult> CompleteChallenges(Guid sessionId) {
var result = await _certsFlowService.CompleteChallengesAsync(sessionId);
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")]
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();
}
/// <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")]
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();
}
/// <summary>
/// Apply certificates from local cache to remote server
/// </summary>
/// <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);
[HttpPost("{accountId}/certificates/apply")]
public async Task<IActionResult> ApplyCertificates(Guid accountId) {
var result = await _certsFlowService.ApplyCertificatesAsync(accountId);
return result.ToActionResult();
}
/// <summary>
/// Revoke certificates
/// </summary>
/// <param name="sessionId"></param>
/// <param name="requestData"></param>
/// <returns></returns>
[HttpPost("{sessionId}/certificates/revoke")]
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();
}
}

View File

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

View File

@ -9,7 +9,7 @@
</PropertyGroup>
<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.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />

View File

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

View File

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

View File

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

View File

@ -1,52 +1,31 @@
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.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
using MaksIT.LetsEncrypt.Services;
namespace MaksIT.LetsEncryptServer.Services;
public interface ICertsCommonService {
public interface ICertsFlowService {
Result<string?> GetTermsOfService(Guid sessionId);
Task<Result> CompleteChallengesAsync(Guid sessionId);
}
public interface ICertsInternalService : ICertsCommonService {
Task<Result<Guid?>> ConfigureClientAsync(bool isStaging);
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> GetOrderAsync(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<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);
}
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);
}
public interface ICertsFlowService
: ICertsInternalService,
ICertsRestService,
ICertsRestChallengeService { }
public class CertsFlowService : ICertsFlowService {
private readonly Configuration _appSettings;
private readonly ILogger<CertsFlowService> _logger;
private readonly HttpClient _httpClient;
private readonly ILetsEncryptService _letsEncryptService;
private readonly ICacheService _cacheService;
private readonly IAgentService _agentService;
@ -55,42 +34,55 @@ public class CertsFlowService : ICertsFlowService {
public CertsFlowService(
IOptions<Configuration> appSettings,
ILogger<CertsFlowService> logger,
HttpClient httpClient,
ILetsEncryptService letsEncryptService,
ICacheService cashService,
IAgentService agentService
) {
_appSettings = appSettings.Value;
_logger = logger;
_httpClient = httpClient;
_letsEncryptService = letsEncryptService;
_cacheService = cashService;
_agentService = agentService;
_acmePath = _appSettings.AcmeFolder;
}
#region Common methods
public Result<string?> GetTermsOfService(Guid sessionId) {
var result = _letsEncryptService.GetTermsOfServiceUri(sessionId);
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) {
return await _letsEncryptService.CompleteChallenges(sessionId);
}
#endregion
#region Internal methods
public async Task<Result<Guid?>> ConfigureClientAsync(bool isStaging) {
var sessionId = Guid.NewGuid();
var result = await _letsEncryptService.ConfigureClient(sessionId, isStaging);
if (!result.IsSuccess)
return result.ToResultOfType<Guid?>(default);
return Result<Guid?>.Ok(sessionId);
}
public async Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts) {
RegistrationCache? cache = null;
if (accountId == null) {
accountId = Guid.NewGuid();
}
@ -103,8 +95,8 @@ public class CertsFlowService : ICertsFlowService {
else {
cache = cacheResult.Value;
}
}
var result = await _letsEncryptService.Init(sessionId, accountId.Value, description, contacts, cache);
if (!result.IsSuccess)
return result.ToResultOfType<Guid?>(default);
@ -151,20 +143,20 @@ public class CertsFlowService : ICertsFlowService {
return await _letsEncryptService.GetOrder(sessionId, hostnames);
}
public async Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid sessionId, string[] hostnames) {
var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId);
public async Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid accountId) {
var cacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!cacheResult.IsSuccess || cacheResult.Value?.CachedCerts == null)
return cacheResult.ToResultOfType<Dictionary<string, string>?>(_ => null);
var results = new Dictionary<string, string>();
foreach (var hostname in hostnames) {
CertificateCache? cert;
var cache = cacheResult.Value;
var results = cache.GetCertsPemPerHostname();
if (cacheResult.Value.TryGetCachedCertificate(hostname, out cert)) {
var content = $"{cert.Cert}\n{cert.PrivatePem}";
results.Add(hostname, content);
}
}
if (cache.IsDisabled)
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);
if (!uploadResult.IsSuccess)
@ -224,9 +216,8 @@ public class CertsFlowService : ICertsFlowService {
if (!certsResult.IsSuccess)
return certsResult.ToResultOfType<Guid?>(default);
// Bypass applying certificates in staging mode
if (!isStaging) {
var applyCertsResult = await ApplyCertificatesAsync(sessionId, hostnames);
var applyCertsResult = await ApplyCertificatesAsync(accountId ?? Guid.Empty);
if (!applyCertsResult.IsSuccess)
return applyCertsResult.ToResultOfType<Guid?>(_ => null);
}
@ -251,32 +242,7 @@ public class CertsFlowService : ICertsFlowService {
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) {
DeleteExporedChallenges();
@ -295,6 +261,7 @@ public class CertsFlowService : ICertsFlowService {
var creationTime = File.GetCreationTime(file);
var timeDifference = currentDate - creationTime;
if (timeDifference.TotalDays > 1) {
File.Delete(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",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-pdf": "^10.2.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.7.1",
"react-virtualized": "^9.22.6",
@ -769,6 +770,191 @@
"@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": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1761,7 +1947,6 @@
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -1849,7 +2034,6 @@
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
@ -2095,7 +2279,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -2674,6 +2857,15 @@
"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": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -2965,7 +3157,6 @@
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@ -4502,6 +4693,24 @@
"@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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -4511,6 +4720,23 @@
"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": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -4866,6 +5092,18 @@
"dev": true,
"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": {
"version": "1.1.1",
"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",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -5021,7 +5258,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@ -5041,12 +5277,49 @@
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
"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": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@ -5125,8 +5398,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@ -5667,8 +5939,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.2",
@ -5698,6 +5969,12 @@
"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": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@ -5736,7 +6013,6 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -5867,7 +6143,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -5964,7 +6239,6 @@
"integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.6",
@ -6055,7 +6329,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -6063,6 +6336,15 @@
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

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

View File

@ -9,6 +9,7 @@ import { UserButton } from './components/UserButton'
import { Toast } from './components/Toast'
import { UtilitiesPage } from './pages/UtilitiesPage'
import { RegisterPage } from './pages/RegisterPage'
import { LetsEncryptTermsOfServicePage } from './pages/LetsEncryptTermsOfServicePage'
interface LayoutWrapperProps {
@ -83,6 +84,12 @@ const AppMap: AppMapType[] = [
routes: ['/utilities'],
page: UtilitiesPage,
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_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
generateSecret = 'GET|/secret/generatesecret',

View File

@ -4,6 +4,11 @@ import { ApiRoutes, GetApiRoute } from './AppMap'
import { store } from './redux/store'
import { refreshJwt } from './redux/slices/identitySlice'
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
const axiosInstance = axios.create({
@ -73,19 +78,35 @@ axiosInstance.interceptors.response.use(
error => {
// Handle response error
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)
}
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)
}
)
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 {
const response = await axiosInstance.get<TResponse>(url, {
headers: {
'Content-Type': 'application/json'
}
},
...(timeout ? { timeout } : {})
})
return response.data
} 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 {
const response = await axiosInstance.post<TResponse>(url, data, {
headers: {
'Content-Type': 'application/json'
}
},
...(timeout ? { timeout } : {})
})
return response.data
} catch {
// 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 {
const response = await axiosInstance.patch<TResponse>(url, data, {
headers: {
'Content-Type': 'application/json'
}
},
...(timeout ? { timeout } : {})
})
return response.data
} 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 {
const response = await axiosInstance.put<TResponse>(url, data, {
headers: {
'Content-Type': 'application/json'
}
},
...(timeout ? { timeout } : {})
})
return response.data
} 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 {
const response = await axiosInstance.delete<TResponse>(url, {
headers: {
'Content-Type': 'application/json'
}
},
...(timeout ? { timeout } : {})
})
return response.data
} catch {

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { parseISO, formatISO, format, getDaysInMonth, addMonths, subMonths } fro
import { ButtonComponent } from './ButtonComponent'
import { TextBoxComponent } from './TextBoxComponent'
import { CircleX } from 'lucide-react'
import { FieldContainer } from './FieldContainer'
const DISPLAY_FORMAT = 'yyyy-MM-dd HH:mm'
@ -119,9 +120,8 @@ const DateTimePickerComponent: FC<DateTimePickerComponentProps> = ({
}, [showDropdown])
return (
<div className={`relative mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`} ref={dropdownRef}>
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
<div className={'relative'}>
<FieldContainer colspan={colspan} label={label} errorText={errorText}>
<div className={'relative'} ref={dropdownRef}>
<input
type={'text'}
value={value ? formatForDisplay(parsedValue!) : ''}
@ -138,7 +138,6 @@ const DateTimePickerComponent: FC<DateTimePickerComponentProps> = ({
<div className={'absolute top-0 bottom-0 right-2 flex items-center gap-1 pointer-events-auto'}>
{actionButtons()}
</div>
</div>
{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'}>
@ -184,8 +183,8 @@ const DateTimePickerComponent: FC<DateTimePickerComponentProps> = ({
</div>
</div>
)}
{errorText && <p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>}
</div>
</FieldContainer>
)
}

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { debounce } from 'lodash'
import { CircleX } from 'lucide-react'
import { ChangeEvent, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FieldContainer } from './FieldContainer'
export interface SelectBoxComponentOption {
value: string | number
@ -167,9 +168,9 @@ const SelectBoxComponent: FC<SelectBoxComponentProps> = (props) => {
}
return (
<div className={`relative mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}>
{/* Label for the select input */}
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
<FieldContainer colspan={colspan} label={label} errorText={errorText}>
<div className={'relative'}>
<div className={'relative'}>
<input
type={'text'}
@ -212,8 +213,8 @@ const SelectBoxComponent: FC<SelectBoxComponentProps> = (props) => {
)}
</div>
)}
{errorText && <p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>}
</div>
</FieldContainer>
)
}

View File

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

View File

@ -1,3 +1,7 @@
import {
FieldContainer,
} from './FieldContainer'
import { ButtonComponent } from './ButtonComponent'
import { CheckBoxComponent } from './CheckBoxComponent'
import { TextBoxComponent } from './TextBoxComponent'
@ -13,6 +17,7 @@ import { FileUploadComponent } from './FileUploadComponent'
export {
FieldContainer as EditorWrapper,
ButtonComponent,
CheckBoxComponent,
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 { ButtonComponent, CheckBoxComponent, RadioGroupComponent, SelectBoxComponent } from '../components/editors'
import { CacheAccount } from '../entities/CacheAccount'
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 { enumToArr, formatISODateString } from '../functions'
import { ChallengeType } from '../entities/ChallengeType'
import { Radio } from 'lucide-react'
import { formatISODateString } from '../functions'
import { addToast } from '../components/Toast/addToast'
import { Offcanvas } from '../components/Offcanvas'
import { EditAccount } from './EditAccount'
const Home: FC = () => {
const [rawd, setRawd] = useState<GetAccountResponse[]>([])
const [editingAccount, setEditingAccount] = useState<GetAccountResponse | null>(
null
)
useEffect(() => {
console.log(GetApiRoute(ApiRoutes.ACCOUNTS).route)
const [accountId, setAccountId] = useState<string | undefined>(undefined)
const loadData = useCallback(() => {
getData<GetAccountResponse[]>(GetApiRoute(ApiRoutes.ACCOUNTS).route).then((response) => {
if (!response) return
setRawd(response)
})
}, [])
useEffect(() => {
loadData()
}, [loadData])
const handleAccountUpdate = (updatedAccount: CacheAccount) => {
// setAccounts(
// accounts.map((account) =>
@ -37,18 +36,36 @@ const Home: FC = () => {
// )
}
const deleteAccount = (accountId: string) => {
const handleDeleteAccount = (accountId: string) => {
deleteData<void>(
GetApiRoute(ApiRoutes.ACCOUNT_DELETE)
.route.replace('{accountId}', accountId)
).then((result) => {
if (!result) return
).then(_ => {
setRawd(rawd.filter((account) => account.accountId !== accountId))
})
}
return <FormContainer>
const handleEditCancel = () => {
setAccountId(undefined)
}
const handleRedeployCerts = (accountId: string) => {
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'}>
@ -59,18 +76,24 @@ const Home: FC = () => {
rawd.map((acc) => (
<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'}>
<h2 className={'col-span-8'}>
<h2 className={'col-span-6'}>
Account: {acc.accountId}
</h2>
<ButtonComponent
colspan={2}
onClick={() => deleteAccount(acc.accountId)}
label={'Delete'}
onClick={() => handleDeleteAccount(acc.accountId)}
label={'Delete Account'}
buttonHierarchy={'error'}
/>
<ButtonComponent
colspan={2}
onClick={() => setEditingAccount(acc)}
children={'Redeploy certs'}
buttonHierarchy={'success'}
onClick={() => handleRedeployCerts(acc.accountId)}
/>
<ButtonComponent
colspan={2}
onClick={() => setAccountId(acc.accountId)}
label={'Edit'}
/>
<h3 className={'col-span-12'}>
@ -97,7 +120,6 @@ const Home: FC = () => {
{ value: 'staging', label: 'Staging' },
{ value: 'production', label: 'Production' }
]}
value={acc.challengeType ? 'staging' : 'production'}
disabled={true}
/>
@ -106,7 +128,7 @@ const Home: FC = () => {
{acc.hostnames?.map((hostname) => (
<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'}>{formatISODateString(hostname.expires)}</span>
<span className={'col-span-3'}>Exp: {formatISODateString(hostname.expires)}</span>
<span className={'col-span-3'}>
<span className={`${hostname.isUpcomingExpire
? 'bg-yellow-200 text-yellow-800'
@ -115,7 +137,6 @@ const Home: FC = () => {
: 'Not Upcoming'}
</span>
</span>
<CheckBoxComponent
colspan={3}
value={hostname.isDisabled}
@ -126,7 +147,6 @@ const Home: FC = () => {
</li>
))}
</ul>
<SelectBoxComponent
label={'Environment'}
options={[
@ -139,16 +159,21 @@ const Home: FC = () => {
</div>
</div>
))}
</div>
</FormContent>
<FormFooter />
</FormContainer>
<Offcanvas isOpen={accountId !== undefined}>
{accountId && <EditAccount
accountId={accountId}
cancelEnabled={true}
onSubmitted={handleOnSubmitted}
onCancel={handleEditCancel}
/>}
</Offcanvas>
</>
}
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 { addToast } from '../components/Toast/addToast'
import { useNavigate } from 'react-router-dom'
import { PlusIcon, TrashIcon } from 'lucide-react'
import { FieldContainer } from '../components/editors/FieldContainer'
interface RegisterFormProps {
@ -90,7 +92,7 @@ const Register: FC<RegisterProps> = () => {
return
}
postData<PostAccountRequest, GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_POST).route, request.data)
postData<PostAccountRequest, GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_POST).route, request.data, 120000)
.then(response => {
if (!response) return
@ -115,14 +117,16 @@ const Register: FC<RegisterProps> = () => {
{formState.contacts.map((contact) => (
<li key={contact} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
<span className={'col-span-10'}>{contact}</span>
<FieldContainer colspan={2}>
<ButtonComponent
colspan={2}
label={'TRASH'}
onClick={() => {
const updatedContacts = formState.contacts.filter(c => c !== contact)
handleInputChange('contacts', updatedContacts)
}}
/>
>
<TrashIcon />
</ButtonComponent>
</FieldContainer>
</li>
))}
</ul>
@ -140,19 +144,23 @@ const Register: FC<RegisterProps> = () => {
type={'text'}
errorText={errors.contact}
/>
<FieldContainer colspan={2}>
<ButtonComponent
colspan={2}
label={'PLUS'}
onClick={() => {
handleInputChange('contacts', [...formState.contacts, formState.contact])
handleInputChange('contact', '')
}}
disabled={formState.contact.trim() === ''}
/>
>
<PlusIcon />
</ButtonComponent>
</FieldContainer>
<div className={'col-span-12'}>
<SelectBoxComponent
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}
placeholder={'Select Challenge Type'}
onChange={(e) => handleInputChange('challengeType', e.target.value)}
@ -164,14 +172,16 @@ const Register: FC<RegisterProps> = () => {
{formState.hostnames.map((hostname) => (
<li key={hostname} className={'grid grid-cols-12 gap-4 w-full'}>
<span className={'col-span-10'}>{hostname}</span>
<FieldContainer colspan={2}>
<ButtonComponent
colspan={2}
label={'TRASH'}
onClick={() => {
const updatedHostnames = formState.hostnames.filter(h => h !== hostname)
handleInputChange('hostnames', updatedHostnames)
}}
/>
>
<TrashIcon />
</ButtonComponent>
</FieldContainer>
</li>
))}
</ul>
@ -189,15 +199,17 @@ const Register: FC<RegisterProps> = () => {
type={'text'}
errorText={errors.hostname}
/>
<FieldContainer colspan={2}>
<ButtonComponent
colspan={2}
label={'PLUS'}
onClick={() => {
handleInputChange('hostnames', [...formState.hostnames, formState.hostname])
handleInputChange('hostname', '')
}}
disabled={formState.hostname.trim() === ''}
/>
>
<PlusIcon />
</ButtonComponent>
</FieldContainer>
<RadioGroupComponent
colspan={12}
label={'LetsEncrypt Environment'}

View File

@ -1,11 +1,23 @@
import { FC, useState } from 'react'
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 [files, setFiles] = useState<File[]>([])
const hadnleTestAgent = () => {
getData(GetApiRoute(ApiRoutes.AGENT_TEST).route)
.then((response) => {
if (!response) return
addToast(response?.message, 'info')
})
}
return <FormContainer>
<FormHeader>Utilities</FormHeader>
<FormContent>
@ -13,15 +25,8 @@ const Utilities: FC = () => {
<ButtonComponent
colspan={3}
label={'Test agent'}
buttonHierarchy={'primary'}
onClick={() => {}}
/>
<ButtonComponent
colspan={3}
label={'Download cache files'}
buttonHierarchy={'secondary'}
onClick={() => {}}
buttonHierarchy={'warning'}
onClick={hadnleTestAgent}
/>
<FileUploadComponent
@ -31,9 +36,20 @@ const Utilities: FC = () => {
onChange={setFiles}
/>
<span className={'col-span-12'}></span>
</div>
<ButtonComponent
colspan={3}
children={'Download cache files'}
buttonHierarchy={'secondary'}
onClick={() => {}}
/>
<ButtonComponent
colspan={3}
children={'Destroy cache files'}
buttonHierarchy={'error'}
onClick={() => {}}
/>
</div>
</FormContent>
<FormFooter />
</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'
import { deepEqualArrays, deepEqual } from './deepEqual'
import { deepMerge } from './deepMerge'
import { deepPatternMatch } from './deepPatternMatch'
export {
@ -14,5 +14,6 @@ export {
deltaHasOperations,
deepEqualArrays,
deepEqual,
deepMerge
deepMerge,
deepPatternMatch
}

View File

@ -9,6 +9,7 @@ import {
deltaHasOperations,
deepEqual,
deepMerge,
deepPatternMatch
} from './deep'
import {
@ -39,6 +40,7 @@ export {
deltaHasOperations,
deepEqual,
deepMerge,
deepPatternMatch,
enumToArr,
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 }