mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): edit components improvements, problem+json messages, account edit form init, redeploy certs controller, interfaces review
This commit is contained in:
parent
edacd27aef
commit
494fcc0f9a
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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>();
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
312
src/MaksIT.WebUI/package-lock.json
generated
312
src/MaksIT.WebUI/package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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',
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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'}>
|
||||||
<
|
<
|
||||||
</button>
|
</button>
|
||||||
<span>{format(currentViewDate, 'MMMM yyyy')}</span>
|
<span>{format(currentViewDate, 'MMMM yyyy')}</span>
|
||||||
<button onClick={handleNextMonth} type={'button'}>
|
<button onClick={handleNextMonth} type={'button'}>
|
||||||
>
|
>
|
||||||
</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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
src/MaksIT.WebUI/src/components/editors/FieldContainer.tsx
Normal file
28
src/MaksIT.WebUI/src/components/editors/FieldContainer.tsx
Normal 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
|
||||||
|
}
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
96
src/MaksIT.WebUI/src/forms/EditAccount.tsx
Normal file
96
src/MaksIT.WebUI/src/forms/EditAccount.tsx
Normal 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
|
||||||
|
}
|
||||||
@ -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 }
|
||||||
120
src/MaksIT.WebUI/src/forms/LetsEncryptTermsOfService.tsx
Normal file
120
src/MaksIT.WebUI/src/forms/LetsEncryptTermsOfService.tsx
Normal 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 }
|
||||||
@ -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'}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
16
src/MaksIT.WebUI/src/functions/deep/deepPatternMatch.ts
Normal file
16
src/MaksIT.WebUI/src/functions/deep/deepPatternMatch.ts
Normal 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
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
15
src/MaksIT.WebUI/src/models/ProblemDetails.ts
Normal file
15
src/MaksIT.WebUI/src/models/ProblemDetails.ts
Normal 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: {}
|
||||||
|
})
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { LetsEncryptTermsOfService } from '../forms/LetsEncryptTermsOfService'
|
||||||
|
|
||||||
|
|
||||||
|
const LetsEncryptTermsOfServicePage = () => {
|
||||||
|
return <LetsEncryptTermsOfService />
|
||||||
|
}
|
||||||
|
export { LetsEncryptTermsOfServicePage }
|
||||||
Loading…
Reference in New Issue
Block a user