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)
|
||||
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>
|
||||
<PackageReference Include="MaksIT.Core" Version="1.5.1" />
|
||||
<PackageReference Include="MaksIT.Results" Version="1.1.0" />
|
||||
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||
|
||||
@ -11,7 +11,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
||||
private readonly IOptions<Configuration> _appSettings;
|
||||
private readonly ILogger<AutoRenewal> _logger;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ICertsInternalService _certsFlowService;
|
||||
private readonly ICertsFlowService _certsFlowService;
|
||||
|
||||
public AutoRenewal(
|
||||
IOptions<Configuration> appSettings,
|
||||
|
||||
@ -1,127 +1,71 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using MaksIT.LetsEncryptServer.Services;
|
||||
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Controllers {
|
||||
|
||||
/// <summary>
|
||||
/// Certificates flow controller, used for granular testing purposes
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/certs")]
|
||||
public class CertsFlowController : ControllerBase {
|
||||
|
||||
private readonly ICertsFlowService _certsFlowService;
|
||||
|
||||
public CertsFlowController(
|
||||
ICertsFlowService certsFlowService
|
||||
) {
|
||||
public CertsFlowController(ICertsFlowService certsFlowService) {
|
||||
_certsFlowService = certsFlowService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize certificate flow session
|
||||
/// </summary>
|
||||
/// <returns>sessionId</returns>
|
||||
[HttpPost("configure-client")]
|
||||
public async Task<IActionResult> ConfigureClient([FromBody] ConfigureClientRequest requestData) {
|
||||
var result = await _certsFlowService.ConfigureClientAsync(requestData);
|
||||
var result = await _certsFlowService.ConfigureClientAsync(requestData.IsStaging);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves terms of service
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session ID</param>
|
||||
/// <returns>Terms of service</returns>
|
||||
[HttpGet("{sessionId}/terms-of-service")]
|
||||
public IActionResult TermsOfService(Guid sessionId) {
|
||||
var result = _certsFlowService.GetTermsOfService(sessionId);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a new certificate session is created, create or retrieve cache data by accountId
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session ID</param>
|
||||
/// <param name="accountId">Account ID</param>
|
||||
/// <param name="requestData">Request data</param>
|
||||
/// <returns>Account ID</returns>
|
||||
[HttpPost("{sessionId}/init/{accountId?}")]
|
||||
public async Task<IActionResult> Init(Guid sessionId, Guid? accountId, [FromBody] InitRequest requestData) {
|
||||
var result = await _certsFlowService.InitAsync(sessionId, accountId, requestData);
|
||||
var result = await _certsFlowService.InitAsync(sessionId, accountId, requestData.Description, requestData.Contacts);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// After account initialization, create a new order request
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session ID</param>
|
||||
/// <param name="requestData">Request data</param>
|
||||
/// <returns>New order response</returns>
|
||||
[HttpPost("{sessionId}/order")]
|
||||
public async Task<IActionResult> NewOrder(Guid sessionId, [FromBody] NewOrderRequest requestData) {
|
||||
var result = await _certsFlowService.NewOrderAsync(sessionId, requestData);
|
||||
var result = await _certsFlowService.NewOrderAsync(sessionId, requestData.Hostnames, requestData.ChallengeType);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Complete challenges for the new order
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session ID</param>
|
||||
/// <returns>Challenges completion response</returns>
|
||||
[HttpPost("{sessionId}/complete-challenges")]
|
||||
public async Task<IActionResult> CompleteChallenges(Guid sessionId) {
|
||||
var result = await _certsFlowService.CompleteChallengesAsync(sessionId);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get order status before certificate retrieval
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session ID</param>
|
||||
/// <param name="requestData">Request data</param>
|
||||
/// <returns>Order status</returns>
|
||||
[HttpGet("{sessionId}/order-status")]
|
||||
public async Task<IActionResult> GetOrder(Guid sessionId, [FromBody] GetOrderRequest requestData) {
|
||||
var result = await _certsFlowService.GetOrderAsync(sessionId, requestData);
|
||||
var result = await _certsFlowService.GetOrderAsync(sessionId, requestData.Hostnames);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download certificates to local cache
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session ID</param>
|
||||
/// <param name="requestData">Request data</param>
|
||||
/// <returns>Certificates download response</returns>
|
||||
[HttpPost("{sessionId}/certificates/download")]
|
||||
public async Task<IActionResult> GetCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
|
||||
var result = await _certsFlowService.GetCertificatesAsync(sessionId, requestData);
|
||||
var result = await _certsFlowService.GetCertificatesAsync(sessionId, requestData.Hostnames);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply certificates from local cache to remote server
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session ID</param>
|
||||
/// <param name="requestData">Request data</param>
|
||||
/// <returns>Certificates application response</returns>
|
||||
[HttpPost("{sessionId}/certificates/apply")]
|
||||
public async Task<IActionResult> ApplyCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
|
||||
var result = await _certsFlowService.ApplyCertificatesAsync(sessionId, requestData);
|
||||
[HttpPost("{accountId}/certificates/apply")]
|
||||
public async Task<IActionResult> ApplyCertificates(Guid accountId) {
|
||||
var result = await _certsFlowService.ApplyCertificatesAsync(accountId);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revoke certificates
|
||||
/// </summary>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="requestData"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("{sessionId}/certificates/revoke")]
|
||||
public async Task<IActionResult> RevokeCertificates(Guid sessionId, [FromBody] RevokeCertificatesRequest requestData) {
|
||||
var result = await _certsFlowService.RevokeCertificatesAsync(sessionId, requestData);
|
||||
var result = await _certsFlowService.RevokeCertificatesAsync(sessionId, requestData.Hostnames);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ namespace MaksIT.LetsEncryptServer.Controllers;
|
||||
[Route(".well-known")]
|
||||
public class WellKnownController : ControllerBase {
|
||||
|
||||
private readonly ICertsRestChallengeService _certsFlowService;
|
||||
private readonly ICertsFlowService _certsFlowService;
|
||||
|
||||
public WellKnownController(
|
||||
IOptions<Configuration> appSettings,
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MaksIT.Results" Version="1.1.0" />
|
||||
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
|
||||
@ -46,7 +46,7 @@ builder.Services.AddMemoryCache();
|
||||
builder.Services.RegisterLetsEncrypt(appSettings);
|
||||
|
||||
builder.Services.AddSingleton<ICacheService, CacheService>();
|
||||
builder.Services.AddSingleton<ICertsFlowService, CertsFlowService>();
|
||||
builder.Services.AddHttpClient<ICertsFlowService, CertsFlowService>();
|
||||
builder.Services.AddSingleton<IAccountService, AccountService>();
|
||||
builder.Services.AddHttpClient<IAgentService, AgentService>();
|
||||
builder.Services.AddHostedService<AutoRenewal>();
|
||||
|
||||
@ -27,7 +27,7 @@ public class AccountService : IAccountService {
|
||||
|
||||
private readonly ILogger<CacheService> _logger;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ICertsInternalService _certsFlowService;
|
||||
private readonly ICertsFlowService _certsFlowService;
|
||||
|
||||
public AccountService(
|
||||
ILogger<CacheService> logger,
|
||||
|
||||
@ -86,13 +86,13 @@ public class CacheService : ICacheService, IDisposable {
|
||||
return Result<RegistrationCache?>.InternalServerError(null, message);
|
||||
}
|
||||
|
||||
var cache = JsonSerializer.Deserialize<RegistrationCache>(json);
|
||||
var cache = json.ToObject<RegistrationCache>();
|
||||
return Result<RegistrationCache?>.Ok(cache);
|
||||
}
|
||||
|
||||
private async Task<Result> SaveToCacheInternalAsync(Guid accountId, RegistrationCache cache) {
|
||||
var cacheFilePath = GetCacheFilePath(accountId);
|
||||
var json = JsonSerializer.Serialize(cache);
|
||||
var json = cache.ToJson();
|
||||
await File.WriteAllTextAsync(cacheFilePath, json);
|
||||
_logger.LogInformation($"Cache file saved for account {accountId}");
|
||||
return Result.Ok();
|
||||
@ -114,7 +114,6 @@ public class CacheService : ICacheService, IDisposable {
|
||||
|
||||
public async Task<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId) {
|
||||
return await _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
|
||||
|
||||
}
|
||||
|
||||
public async Task<Result> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
|
||||
|
||||
@ -1,52 +1,31 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.LetsEncrypt.Services;
|
||||
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
||||
using MaksIT.Results;
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
||||
using MaksIT.LetsEncrypt.Services;
|
||||
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
public interface ICertsCommonService {
|
||||
public interface ICertsFlowService {
|
||||
Result<string?> GetTermsOfService(Guid sessionId);
|
||||
Task<Result> CompleteChallengesAsync(Guid sessionId);
|
||||
}
|
||||
|
||||
public interface ICertsInternalService : ICertsCommonService {
|
||||
Task<Result<Guid?>> ConfigureClientAsync(bool isStaging);
|
||||
Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts);
|
||||
Task<Result<List<string>?>> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType);
|
||||
Task<Result> GetOrderAsync(Guid sessionId, string[] hostnames);
|
||||
Task<Result> GetCertificatesAsync(Guid sessionId, string[] hostnames);
|
||||
Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid sessionId, string[] hostnames);
|
||||
Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid accountId);
|
||||
Task<Result> RevokeCertificatesAsync(Guid sessionId, string[] hostnames);
|
||||
Task<Result<Guid?>> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames);
|
||||
Task<Result> FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames);
|
||||
}
|
||||
|
||||
public interface ICertsRestService : ICertsCommonService {
|
||||
Task<Result<Guid?>> ConfigureClientAsync(ConfigureClientRequest requestData);
|
||||
Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData);
|
||||
Task<Result<List<string>?>> NewOrderAsync(Guid sessionId, NewOrderRequest requestData);
|
||||
Task<Result> GetOrderAsync(Guid sessionId, GetOrderRequest requestData);
|
||||
Task<Result> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
|
||||
Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
|
||||
Task<Result> RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData);
|
||||
}
|
||||
|
||||
public interface ICertsRestChallengeService {
|
||||
Result<string?> AcmeChallenge(string fileName);
|
||||
}
|
||||
|
||||
public interface ICertsFlowService
|
||||
: ICertsInternalService,
|
||||
ICertsRestService,
|
||||
ICertsRestChallengeService { }
|
||||
|
||||
public class CertsFlowService : ICertsFlowService {
|
||||
private readonly Configuration _appSettings;
|
||||
private readonly ILogger<CertsFlowService> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILetsEncryptService _letsEncryptService;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly IAgentService _agentService;
|
||||
@ -55,42 +34,55 @@ public class CertsFlowService : ICertsFlowService {
|
||||
public CertsFlowService(
|
||||
IOptions<Configuration> appSettings,
|
||||
ILogger<CertsFlowService> logger,
|
||||
HttpClient httpClient,
|
||||
ILetsEncryptService letsEncryptService,
|
||||
ICacheService cashService,
|
||||
IAgentService agentService
|
||||
) {
|
||||
_appSettings = appSettings.Value;
|
||||
_logger = logger;
|
||||
_httpClient = httpClient;
|
||||
_letsEncryptService = letsEncryptService;
|
||||
_cacheService = cashService;
|
||||
_agentService = agentService;
|
||||
_acmePath = _appSettings.AcmeFolder;
|
||||
}
|
||||
|
||||
#region Common methods
|
||||
public Result<string?> GetTermsOfService(Guid sessionId) {
|
||||
var result = _letsEncryptService.GetTermsOfServiceUri(sessionId);
|
||||
if (!result.IsSuccess || result.Value == null)
|
||||
return result;
|
||||
|
||||
var termsOfServiceUrl = result.Value;
|
||||
|
||||
try {
|
||||
var pdfBytesTask = _httpClient.GetByteArrayAsync(termsOfServiceUrl);
|
||||
pdfBytesTask.Wait();
|
||||
var pdfBytes = pdfBytesTask.Result;
|
||||
var base64 = Convert.ToBase64String(pdfBytes);
|
||||
return Result<string?>.Ok(base64);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "Failed to download or convert Terms of Service PDF");
|
||||
return Result<string?>.InternalServerError(null, $"Failed to download or convert Terms of Service PDF: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> CompleteChallengesAsync(Guid sessionId) {
|
||||
return await _letsEncryptService.CompleteChallenges(sessionId);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Internal methods
|
||||
public async Task<Result<Guid?>> ConfigureClientAsync(bool isStaging) {
|
||||
var sessionId = Guid.NewGuid();
|
||||
|
||||
var result = await _letsEncryptService.ConfigureClient(sessionId, isStaging);
|
||||
if (!result.IsSuccess)
|
||||
return result.ToResultOfType<Guid?>(default);
|
||||
|
||||
return Result<Guid?>.Ok(sessionId);
|
||||
}
|
||||
|
||||
public async Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts) {
|
||||
RegistrationCache? cache = null;
|
||||
|
||||
if (accountId == null) {
|
||||
accountId = Guid.NewGuid();
|
||||
}
|
||||
@ -103,8 +95,8 @@ public class CertsFlowService : ICertsFlowService {
|
||||
else {
|
||||
cache = cacheResult.Value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var result = await _letsEncryptService.Init(sessionId, accountId.Value, description, contacts, cache);
|
||||
if (!result.IsSuccess)
|
||||
return result.ToResultOfType<Guid?>(default);
|
||||
@ -151,20 +143,20 @@ public class CertsFlowService : ICertsFlowService {
|
||||
return await _letsEncryptService.GetOrder(sessionId, hostnames);
|
||||
}
|
||||
|
||||
public async Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid sessionId, string[] hostnames) {
|
||||
var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId);
|
||||
public async Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid accountId) {
|
||||
var cacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
||||
if (!cacheResult.IsSuccess || cacheResult.Value?.CachedCerts == null)
|
||||
return cacheResult.ToResultOfType<Dictionary<string, string>?>(_ => null);
|
||||
|
||||
var results = new Dictionary<string, string>();
|
||||
foreach (var hostname in hostnames) {
|
||||
CertificateCache? cert;
|
||||
var cache = cacheResult.Value;
|
||||
var results = cache.GetCertsPemPerHostname();
|
||||
|
||||
if (cacheResult.Value.TryGetCachedCertificate(hostname, out cert)) {
|
||||
var content = $"{cert.Cert}\n{cert.PrivatePem}";
|
||||
results.Add(hostname, content);
|
||||
}
|
||||
}
|
||||
|
||||
if (cache.IsDisabled)
|
||||
return Result<Dictionary<string, string>?>.BadRequest(null, $"Account {accountId} is disabled");
|
||||
|
||||
if (cache.IsStaging)
|
||||
return Result<Dictionary<string, string>?>.UnprocessableEntity(null, $"Found certs for {string.Join(',', results.Keys)} (staging environment)");
|
||||
|
||||
var uploadResult = await _agentService.UploadCerts(results);
|
||||
if (!uploadResult.IsSuccess)
|
||||
@ -224,9 +216,8 @@ public class CertsFlowService : ICertsFlowService {
|
||||
if (!certsResult.IsSuccess)
|
||||
return certsResult.ToResultOfType<Guid?>(default);
|
||||
|
||||
// Bypass applying certificates in staging mode
|
||||
if (!isStaging) {
|
||||
var applyCertsResult = await ApplyCertificatesAsync(sessionId, hostnames);
|
||||
var applyCertsResult = await ApplyCertificatesAsync(accountId ?? Guid.Empty);
|
||||
if (!applyCertsResult.IsSuccess)
|
||||
return applyCertsResult.ToResultOfType<Guid?>(_ => null);
|
||||
}
|
||||
@ -251,32 +242,7 @@ public class CertsFlowService : ICertsFlowService {
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region REST methods
|
||||
public async Task<Result<Guid?>> ConfigureClientAsync(ConfigureClientRequest requestData) {
|
||||
return await ConfigureClientAsync(requestData.IsStaging);
|
||||
}
|
||||
public async Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData) {
|
||||
return await InitAsync(sessionId, accountId, requestData.Description, requestData.Contacts);
|
||||
}
|
||||
public async Task<Result<List<string>>> NewOrderAsync(Guid sessionId, NewOrderRequest requestData) {
|
||||
return await NewOrderAsync(sessionId, requestData.Hostnames, requestData.ChallengeType);
|
||||
}
|
||||
public async Task<Result> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) {
|
||||
return await GetCertificatesAsync(sessionId, requestData.Hostnames);
|
||||
}
|
||||
public async Task<Result> GetOrderAsync(Guid sessionId, GetOrderRequest requestData) {
|
||||
return await GetOrderAsync(sessionId, requestData.Hostnames);
|
||||
}
|
||||
|
||||
public async Task<Result<Dictionary<string, string>>> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) =>
|
||||
await ApplyCertificatesAsync(sessionId, requestData.Hostnames);
|
||||
public async Task<Result> RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData) =>
|
||||
await RevokeCertificatesAsync(sessionId, requestData.Hostnames);
|
||||
#endregion
|
||||
|
||||
#region Acme Challenge REST methods
|
||||
public Result<string?> AcmeChallenge(string fileName) {
|
||||
DeleteExporedChallenges();
|
||||
|
||||
@ -295,6 +261,7 @@ public class CertsFlowService : ICertsFlowService {
|
||||
var creationTime = File.GetCreationTime(file);
|
||||
var timeDifference = currentDate - creationTime;
|
||||
|
||||
|
||||
if (timeDifference.TotalDays > 1) {
|
||||
File.Delete(file);
|
||||
_logger.LogInformation($"Deleted file: {file}");
|
||||
@ -305,5 +272,4 @@ public class CertsFlowService : ICertsFlowService {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
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",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-pdf": "^10.2.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.7.1",
|
||||
"react-virtualized": "^9.22.6",
|
||||
@ -769,6 +770,191 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.81.tgz",
|
||||
"integrity": "sha512-ReCjd5SYI/UKx/olaQLC4GtN6wUQGjlgHXs1lvUvWGXfBMR3Fxnik3cL+OxKN5ithNdoU0/GlCrdKcQDFh2XKQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.81",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.81",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.81",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.81",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.81",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.81",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.81",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.81",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.81",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.81"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.81.tgz",
|
||||
"integrity": "sha512-78Lz+AUi+MsWupyZjXwpwQrp1QCwncPvRZrdvrROcZ9Gq9grP7LfQZiGdR8LKyHIq3OR18mDP+JESGT15V1nXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.81.tgz",
|
||||
"integrity": "sha512-omejuKgHWKDGoh8rsgsyhm/whwxMaryTQjJTd9zD7hiB9/rzcEEJLHnzXWR5ysy4/tTjHaQotE6k2t8eodTLnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.81.tgz",
|
||||
"integrity": "sha512-EYfk+co6BElq5DXNH9PBLYDYwc4QsvIVbyrsVHsxVpn4p6Y3/s8MChgC69AGqj3vzZBQ1qx2CRCMtg5cub+XuQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.81.tgz",
|
||||
"integrity": "sha512-teh6Q74CyAcH31yLNQGR9MtXSFxlZa5CI6vvNUISI14gWIJWrhOwUAOly+KRe1aztWR0FWTVSPxM4p5y+06aow==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.81.tgz",
|
||||
"integrity": "sha512-AGEopHFYRzJOjxY+2G1RmHPRnuWvO3Qdhq7sIazlSjxb3Z6dZHg7OB/4ZimXaimPjDACm9qWa6t5bn9bhXvkcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.81.tgz",
|
||||
"integrity": "sha512-Bj3m1cl4GIhsigkdwOxii4g4Ump3/QhNpx85IgAlCCYXpaly6mcsWpuDYEabfIGWOWhDUNBOndaQUPfWK1czOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.81.tgz",
|
||||
"integrity": "sha512-yg/5NkHykVdwPlD3XObwCa/EswkOwLHswJcI9rHrac+znHsmCSj5AMX/RTU9Z9F6lZTwL60JM2Esit33XhAMiw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.81.tgz",
|
||||
"integrity": "sha512-tPfMpSEBuV5dJSKexO/UZxpOqnYTaNbG8aKa1ek8QsWu+4SJ/foWkaxscra/RUv85vepx6WWDjzBNbNJsTnO0w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.81.tgz",
|
||||
"integrity": "sha512-1L0xnYgzqn8Baef+inPvY4dKqdmw3KCBoe0NEDgezuBZN7MA5xElwifoG8609uNdrMtJ9J6QZarsslLRVqri7g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.81",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.81.tgz",
|
||||
"integrity": "sha512-57ryVbhm/z7RE9/UVcS7mrLPdlayLesy+9U0Uf6epCoeSGrs99tfieCcgZWFbIgmByQ1AZnNtFI2N6huqDLlWQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@ -1761,7 +1947,6 @@
|
||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@ -1849,7 +2034,6 @@
|
||||
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
@ -2095,7 +2279,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -2674,6 +2857,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
@ -2965,7 +3157,6 @@
|
||||
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@ -4502,6 +4693,24 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-cancellable-promise": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz",
|
||||
"integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-event-props": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz",
|
||||
"integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -4511,6 +4720,23 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-refs": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz",
|
||||
"integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/merge-refs?sponsor=1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@ -4866,6 +5092,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "5.4.296",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
|
||||
"integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20.16.0 || >=22.3.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas": "^0.1.80"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@ -5011,7 +5249,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -5021,7 +5258,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@ -5041,12 +5277,49 @@
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-pdf": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.2.0.tgz",
|
||||
"integrity": "sha512-zk0DIL31oCh8cuQycM0SJKfwh4Onz0/Nwi6wTOjgtEjWGUY6eM+/vuzvOP3j70qtEULn7m1JtaeGzud1w5fY2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"dequal": "^2.0.3",
|
||||
"make-cancellable-promise": "^2.0.0",
|
||||
"make-event-props": "^2.0.0",
|
||||
"merge-refs": "^2.0.0",
|
||||
"pdfjs-dist": "5.4.296",
|
||||
"tiny-invariant": "^1.0.0",
|
||||
"warning": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-pdf/node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@ -5125,8 +5398,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@ -5667,8 +5939,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
@ -5698,6 +5969,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
@ -5736,7 +6013,6 @@
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -5867,7 +6143,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -5964,7 +6239,6 @@
|
||||
"integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.6",
|
||||
@ -6055,7 +6329,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -6063,6 +6336,15 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-pdf": "^10.2.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.7.1",
|
||||
"react-virtualized": "^9.22.6",
|
||||
|
||||
@ -9,6 +9,7 @@ import { UserButton } from './components/UserButton'
|
||||
import { Toast } from './components/Toast'
|
||||
import { UtilitiesPage } from './pages/UtilitiesPage'
|
||||
import { RegisterPage } from './pages/RegisterPage'
|
||||
import { LetsEncryptTermsOfServicePage } from './pages/LetsEncryptTermsOfServicePage'
|
||||
|
||||
|
||||
interface LayoutWrapperProps {
|
||||
@ -83,6 +84,12 @@ const AppMap: AppMapType[] = [
|
||||
routes: ['/utilities'],
|
||||
page: UtilitiesPage,
|
||||
linkArea: [LinkArea.SideMenu]
|
||||
},
|
||||
{
|
||||
title: 'Terms of Service',
|
||||
routes: ['/terms-of-service'],
|
||||
page: LetsEncryptTermsOfServicePage,
|
||||
linkArea: [LinkArea.SideMenu]
|
||||
}
|
||||
|
||||
// {
|
||||
@ -153,6 +160,15 @@ enum ApiRoutes {
|
||||
// ACCOUNT_ID_HOSTNAMES = 'GET|/account/{accountId}/hostnames',
|
||||
// ACCOUNT_ID_HOSTNAME_ID = 'GET|/account/{accountId}/hostname/{index}',
|
||||
|
||||
// Agents
|
||||
AGENT_TEST = 'GET|/agent/test',
|
||||
|
||||
// Certs flow
|
||||
CERTS_FLOW_CONFIGURE_CLIENT = 'POST|/certs/configure-client',
|
||||
CERTS_FLOW_TERMS_OF_SERVICE = 'GET|/certs/{sessionId}/terms-of-service',
|
||||
CERTS_FLOW_CERTIFICATES_APPLY = 'POST|/certs/{accountId}/certificates/apply',
|
||||
|
||||
|
||||
// Secrets
|
||||
generateSecret = 'GET|/secret/generatesecret',
|
||||
|
||||
|
||||
@ -4,6 +4,11 @@ import { ApiRoutes, GetApiRoute } from './AppMap'
|
||||
import { store } from './redux/store'
|
||||
import { refreshJwt } from './redux/slices/identitySlice'
|
||||
import { hideLoader, showLoader } from './redux/slices/loaderSlice'
|
||||
import { addToast } from './components/Toast/addToast'
|
||||
import { de } from 'zod/v4/locales'
|
||||
import { deepPatternMatch } from './functions'
|
||||
import { ProblemDetails, ProblemDetailsProto } from './models/ProblemDetails'
|
||||
import { add } from 'lodash'
|
||||
|
||||
// Create an Axios instance
|
||||
const axiosInstance = axios.create({
|
||||
@ -73,19 +78,35 @@ axiosInstance.interceptors.response.use(
|
||||
error => {
|
||||
// Handle response error
|
||||
store.dispatch(hideLoader())
|
||||
if (error.response && error.response.status === 401) {
|
||||
if (error.response) {
|
||||
if (error.response.status === 401) {
|
||||
// Handle unauthorized error (e.g., redirect to login)
|
||||
}
|
||||
else {
|
||||
const contentType = error.response.headers['content-type']
|
||||
|
||||
if (contentType && contentType.includes('application/problem+json')) {
|
||||
const problem = error.response.data as ProblemDetails
|
||||
addToast(`${problem.title}: ${problem.detail}`, 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
const getData = async <TResponse>(url: string): Promise<TResponse | undefined> => {
|
||||
/**
|
||||
* Performs a GET request and returns the response data.
|
||||
* @param url The endpoint URL.
|
||||
* @param timeout Optional timeout in milliseconds to override the default.
|
||||
*/
|
||||
const getData = async <TResponse>(url: string, timeout?: number): Promise<TResponse | undefined> => {
|
||||
try {
|
||||
const response = await axiosInstance.get<TResponse>(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
},
|
||||
...(timeout ? { timeout } : {})
|
||||
})
|
||||
return response.data
|
||||
} catch {
|
||||
@ -94,13 +115,21 @@ const getData = async <TResponse>(url: string): Promise<TResponse | undefined> =
|
||||
}
|
||||
}
|
||||
|
||||
const postData = async <TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | undefined> => {
|
||||
/**
|
||||
* Performs a POST request with the given data and returns the response data.
|
||||
* @param url The endpoint URL.
|
||||
* @param data The request payload.
|
||||
* @param timeout Optional timeout in milliseconds to override the default.
|
||||
*/
|
||||
const postData = async <TRequest, TResponse>(url: string, data?: TRequest, timeout?: number): Promise<TResponse | undefined> => {
|
||||
try {
|
||||
const response = await axiosInstance.post<TResponse>(url, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
},
|
||||
...(timeout ? { timeout } : {})
|
||||
})
|
||||
|
||||
return response.data
|
||||
} catch {
|
||||
// Error is already handled by interceptors, so just return undefined
|
||||
@ -108,12 +137,19 @@ const postData = async <TRequest, TResponse>(url: string, data: TRequest): Promi
|
||||
}
|
||||
}
|
||||
|
||||
const patchData = async <TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | undefined> => {
|
||||
/**
|
||||
* Performs a PATCH request with the given data and returns the response data.
|
||||
* @param url The endpoint URL.
|
||||
* @param data The request payload.
|
||||
* @param timeout Optional timeout in milliseconds to override the default.
|
||||
*/
|
||||
const patchData = async <TRequest, TResponse>(url: string, data: TRequest, timeout?: number): Promise<TResponse | undefined> => {
|
||||
try {
|
||||
const response = await axiosInstance.patch<TResponse>(url, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
},
|
||||
...(timeout ? { timeout } : {})
|
||||
})
|
||||
return response.data
|
||||
} catch {
|
||||
@ -122,12 +158,19 @@ const patchData = async <TRequest, TResponse>(url: string, data: TRequest): Prom
|
||||
}
|
||||
}
|
||||
|
||||
const putData = async <TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | undefined> => {
|
||||
/**
|
||||
* Performs a PUT request with the given data and returns the response data.
|
||||
* @param url The endpoint URL.
|
||||
* @param data The request payload.
|
||||
* @param timeout Optional timeout in milliseconds to override the default.
|
||||
*/
|
||||
const putData = async <TRequest, TResponse>(url: string, data: TRequest, timeout?: number): Promise<TResponse | undefined> => {
|
||||
try {
|
||||
const response = await axiosInstance.put<TResponse>(url, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
},
|
||||
...(timeout ? { timeout } : {})
|
||||
})
|
||||
return response.data
|
||||
} catch {
|
||||
@ -136,12 +179,18 @@ const putData = async <TRequest, TResponse>(url: string, data: TRequest): Promis
|
||||
}
|
||||
}
|
||||
|
||||
const deleteData = async <TResponse>(url: string): Promise<TResponse | undefined> => {
|
||||
/**
|
||||
* Performs a DELETE request and returns the response data.
|
||||
* @param url The endpoint URL.
|
||||
* @param timeout Optional timeout in milliseconds to override the default.
|
||||
*/
|
||||
const deleteData = async <TResponse>(url: string, timeout?: number): Promise<TResponse | undefined> => {
|
||||
try {
|
||||
const response = await axiosInstance.delete<TResponse>(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
},
|
||||
...(timeout ? { timeout } : {})
|
||||
})
|
||||
return response.data
|
||||
} catch {
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
interface ConditionalButtonProps {
|
||||
label: string;
|
||||
interface CommonButtonProps {
|
||||
colspan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||
route?: string;
|
||||
buttonHierarchy?: 'primary' | 'secondary' | 'success' | 'error' | 'warning';
|
||||
@ -10,10 +9,12 @@ interface ConditionalButtonProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ButtonComponent: React.FC<ConditionalButtonProps> = (props) => {
|
||||
type ButtonComponentProps =
|
||||
| ({ label: string; children?: never } & CommonButtonProps)
|
||||
| ({ children: ReactNode; label?: never } & CommonButtonProps);
|
||||
|
||||
const ButtonComponent: React.FC<ButtonComponentProps> = (props) => {
|
||||
const {
|
||||
label,
|
||||
colspan,
|
||||
route,
|
||||
buttonHierarchy,
|
||||
@ -21,6 +22,9 @@ const ButtonComponent: React.FC<ConditionalButtonProps> = (props) => {
|
||||
disabled = false
|
||||
} = props
|
||||
|
||||
const isChildren = 'children' in props && props.children !== undefined
|
||||
const content = 'label' in props ? props.label : props.children
|
||||
|
||||
const handleClick = (e?: React.MouseEvent) => {
|
||||
if (disabled) {
|
||||
e?.preventDefault()
|
||||
@ -53,25 +57,27 @@ const ButtonComponent: React.FC<ConditionalButtonProps> = (props) => {
|
||||
|
||||
const disabledClass = disabled ? 'opacity-50 cursor-default' : 'cursor-pointer'
|
||||
|
||||
const centeringClass = isChildren ? 'flex justify-center items-center' : 'text-center'
|
||||
|
||||
return route
|
||||
? (
|
||||
<Link
|
||||
to={route}
|
||||
className={`${buttonClass} px-4 py-2 rounded ${colspan ? `col-span-${colspan}` : 'w-full'} text-center ${disabledClass}`}
|
||||
className={`${buttonClass} px-4 py-2 rounded ${colspan ? `col-span-${colspan}` : 'w-full'} ${centeringClass} ${disabledClass}`}
|
||||
onClick={handleClick}
|
||||
tabIndex={disabled ? -1 : undefined}
|
||||
aria-disabled={disabled}
|
||||
style={disabled ? { pointerEvents: 'none' } : undefined}
|
||||
>
|
||||
{label}
|
||||
{content}
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
className={`${buttonClass} px-4 py-2 rounded ${colspan ? `col-span-${colspan}` : 'w-full'} ${disabledClass}`}
|
||||
className={`${buttonClass} px-4 py-2 rounded ${colspan ? `col-span-${colspan}` : 'w-full'} ${centeringClass} ${disabledClass}`}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { FieldContainer } from './FieldContainer'
|
||||
|
||||
interface CheckBoxComponentProps {
|
||||
colspan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||
@ -36,8 +37,7 @@ const CheckBoxComponent: React.FC<CheckBoxComponentProps> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`mb-4 col-span-${colspan}`}>
|
||||
<label className={'block text-gray-700 text-sm font-bold mb-2'}>
|
||||
<FieldContainer colspan={colspan} label={label} errorText={errorText}>
|
||||
<input
|
||||
type={'checkbox'}
|
||||
checked={value}
|
||||
@ -45,14 +45,7 @@ const CheckBoxComponent: React.FC<CheckBoxComponentProps> = (props) => {
|
||||
className={`mr-2 leading-tight ${errorText ? 'border-red-500' : ''}`}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
{errorText && (
|
||||
<p className={'text-red-500 text-xs italic mt-2'}>
|
||||
{errorText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FieldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { parseISO, formatISO, format, getDaysInMonth, addMonths, subMonths } fro
|
||||
import { ButtonComponent } from './ButtonComponent'
|
||||
import { TextBoxComponent } from './TextBoxComponent'
|
||||
import { CircleX } from 'lucide-react'
|
||||
import { FieldContainer } from './FieldContainer'
|
||||
|
||||
const DISPLAY_FORMAT = 'yyyy-MM-dd HH:mm'
|
||||
|
||||
@ -119,9 +120,8 @@ const DateTimePickerComponent: FC<DateTimePickerComponentProps> = ({
|
||||
}, [showDropdown])
|
||||
|
||||
return (
|
||||
<div className={`relative mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`} ref={dropdownRef}>
|
||||
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
|
||||
<div className={'relative'}>
|
||||
<FieldContainer colspan={colspan} label={label} errorText={errorText}>
|
||||
<div className={'relative'} ref={dropdownRef}>
|
||||
<input
|
||||
type={'text'}
|
||||
value={value ? formatForDisplay(parsedValue!) : ''}
|
||||
@ -138,7 +138,6 @@ const DateTimePickerComponent: FC<DateTimePickerComponentProps> = ({
|
||||
<div className={'absolute top-0 bottom-0 right-2 flex items-center gap-1 pointer-events-auto'}>
|
||||
{actionButtons()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showDropdown && !readOnly && !disabled && (
|
||||
<div className={'absolute left-0 right-0 bg-white border border-gray-300 rounded mt-1 w-full shadow-lg z-10'}>
|
||||
@ -184,8 +183,8 @@ const DateTimePickerComponent: FC<DateTimePickerComponentProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{errorText && <p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>}
|
||||
</div>
|
||||
</FieldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react'
|
||||
import { FieldContainer } from './FieldContainer'
|
||||
|
||||
interface DualListboxComponentProps {
|
||||
label?: string;
|
||||
@ -44,10 +45,7 @@ const DualListboxComponent: React.FC<DualListboxComponentProps> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`col-span-${colspan}`}>
|
||||
<label className={'block text-gray-700 text-sm font-bold mb-2'}>
|
||||
{label}
|
||||
</label>
|
||||
<FieldContainer colspan={colspan} label={label} errorText={errorText}>
|
||||
<div className={'flex justify-center items-center gap-4 w-full h-full'}>
|
||||
<div className={'flex flex-col'}>
|
||||
<h3>{availableItemsLabel}</h3>
|
||||
@ -92,12 +90,7 @@ const DualListboxComponent: React.FC<DualListboxComponentProps> = (props) => {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{errorText && (
|
||||
<p className={'text-red-500 text-xs italic mt-2'}>
|
||||
{errorText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FieldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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 { ButtonComponent } from './ButtonComponent'
|
||||
import { TrashIcon } from 'lucide-react'
|
||||
|
||||
interface FileUploadComponentProps {
|
||||
label?: string
|
||||
@ -124,17 +125,18 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||
|
||||
{/* Clear selection button */}
|
||||
<ButtonComponent
|
||||
label={'Clear selection'}
|
||||
buttonHierarchy={'secondary'}
|
||||
onClick={handleClear}
|
||||
disabled={disabled || selectedFiles.length === 0}
|
||||
colspan={1}
|
||||
/>
|
||||
>
|
||||
<TrashIcon />
|
||||
</ButtonComponent>
|
||||
|
||||
{/* Select files button */}
|
||||
<ButtonComponent
|
||||
colspan={2}
|
||||
label={label}
|
||||
children={label}
|
||||
buttonHierarchy={'primary'}
|
||||
onClick={handleSelectFiles}
|
||||
disabled={disabled}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react'
|
||||
import { FieldContainer } from './FieldContainer'
|
||||
|
||||
interface ListboxComponentProps {
|
||||
label?: string;
|
||||
@ -35,10 +36,7 @@ const ListboxComponent: React.FC<ListboxComponentProps> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`col-span-${colspan}`}>
|
||||
<label className={'block text-gray-700 text-sm font-bold mb-2'}>
|
||||
{label}
|
||||
</label>
|
||||
<FieldContainer colspan={colspan} label={label} errorText={errorText}>
|
||||
<div className={'flex flex-col'}>
|
||||
<h3>{itemsLabel}</h3>
|
||||
<ul className={'border p-2 w-40 h-64 overflow-auto'}>
|
||||
@ -53,12 +51,7 @@ const ListboxComponent: React.FC<ListboxComponentProps> = (props) => {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{errorText && (
|
||||
<p className={'text-red-500 text-xs italic mt-2'}>
|
||||
{errorText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FieldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { FieldContainer } from './FieldContainer'
|
||||
|
||||
interface RadioOption {
|
||||
value: string
|
||||
@ -45,8 +46,7 @@ const RadioGroupComponent: React.FC<RadioGroupComponentProps> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}>
|
||||
{label && <label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>}
|
||||
<FieldContainer colspan={colspan} label={label} errorText={errorText}>
|
||||
<div className={'flex flex-col'}>
|
||||
{options.map(option => {
|
||||
// Use default cursor (arrow) if disabled or readOnly, else pointer
|
||||
@ -70,10 +70,7 @@ const RadioGroupComponent: React.FC<RadioGroupComponentProps> = (props) => {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{errorText && (
|
||||
<p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>
|
||||
)}
|
||||
</div>
|
||||
</FieldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { ChangeEvent, FC, useRef, useState } from 'react'
|
||||
import { TrngResponse } from '../../models/TrngResponse'
|
||||
import { getData } from '../../axiosConfig'
|
||||
import { ApiRoutes, GetApiRoute } from '../../AppMap'
|
||||
import { FieldContainer } from './FieldContainer'
|
||||
|
||||
|
||||
interface PasswordGeneratorProps {
|
||||
@ -114,9 +115,7 @@ const SecretComponent: FC<PasswordGeneratorProps> = (props) => {
|
||||
|
||||
|
||||
return (
|
||||
<div className={`mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}>
|
||||
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
|
||||
|
||||
<FieldContainer colspan={colspan} label={label} errorText={errorText}>
|
||||
<div className={'relative'}>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
@ -138,9 +137,7 @@ const SecretComponent: FC<PasswordGeneratorProps> = (props) => {
|
||||
{actionButtons()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorText && <p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>}
|
||||
</div>
|
||||
</FieldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { debounce } from 'lodash'
|
||||
import { CircleX } from 'lucide-react'
|
||||
import { ChangeEvent, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FieldContainer } from './FieldContainer'
|
||||
|
||||
export interface SelectBoxComponentOption {
|
||||
value: string | number
|
||||
@ -167,9 +168,9 @@ const SelectBoxComponent: FC<SelectBoxComponentProps> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}>
|
||||
{/* Label for the select input */}
|
||||
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
|
||||
<FieldContainer colspan={colspan} label={label} errorText={errorText}>
|
||||
<div className={'relative'}>
|
||||
|
||||
<div className={'relative'}>
|
||||
<input
|
||||
type={'text'}
|
||||
@ -212,8 +213,8 @@ const SelectBoxComponent: FC<SelectBoxComponentProps> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{errorText && <p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>}
|
||||
</div>
|
||||
</FieldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { ChangeEvent, FC, useEffect, useRef, useState } from 'react'
|
||||
import { FieldContainer } from './FieldContainer'
|
||||
|
||||
interface TextBoxComponentProps {
|
||||
label: string
|
||||
@ -48,7 +49,7 @@ const TextBoxComponent: FC<TextBoxComponentProps> = (props) => {
|
||||
// Se il type è "textarea", comportamento invariato
|
||||
if (type === 'textarea') {
|
||||
return (
|
||||
<div className={`mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}>
|
||||
<div className={`${colspan ? `col-span-${colspan}` : 'w-full'}`}>
|
||||
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
|
||||
<textarea
|
||||
value={value}
|
||||
@ -69,9 +70,7 @@ const TextBoxComponent: FC<TextBoxComponentProps> = (props) => {
|
||||
const hasContent = String(value).length > 0
|
||||
|
||||
return (
|
||||
<div className={`mb-4 ${colspan ? `col-span-${colspan}` : 'w-full'}`}>
|
||||
<label className={'block text-gray-700 text-sm font-bold mb-2'}>{label}</label>
|
||||
|
||||
<FieldContainer colspan={colspan} label={label} errorText={errorText}>
|
||||
{type === 'password' ? (
|
||||
// Wrapper che contiene input e bottone show/hide, ma bottone solo se c'è contenuto
|
||||
<div className={'relative'}>
|
||||
@ -111,9 +110,7 @@ const TextBoxComponent: FC<TextBoxComponentProps> = (props) => {
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
{errorText && <p className={'text-red-500 text-xs italic mt-2'}>{errorText}</p>}
|
||||
</div>
|
||||
</FieldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
import {
|
||||
FieldContainer,
|
||||
} from './FieldContainer'
|
||||
|
||||
import { ButtonComponent } from './ButtonComponent'
|
||||
import { CheckBoxComponent } from './CheckBoxComponent'
|
||||
import { TextBoxComponent } from './TextBoxComponent'
|
||||
@ -13,6 +17,7 @@ import { FileUploadComponent } from './FileUploadComponent'
|
||||
|
||||
|
||||
export {
|
||||
FieldContainer as EditorWrapper,
|
||||
ButtonComponent,
|
||||
CheckBoxComponent,
|
||||
DateTimePickerComponent,
|
||||
|
||||
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 { ButtonComponent, CheckBoxComponent, RadioGroupComponent, SelectBoxComponent } from '../components/editors'
|
||||
import { CacheAccount } from '../entities/CacheAccount'
|
||||
import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse'
|
||||
import { deleteData, getData } from '../axiosConfig'
|
||||
import { deleteData, getData, postData } from '../axiosConfig'
|
||||
import { ApiRoutes, GetApiRoute } from '../AppMap'
|
||||
import { enumToArr, formatISODateString } from '../functions'
|
||||
import { ChallengeType } from '../entities/ChallengeType'
|
||||
import { Radio } from 'lucide-react'
|
||||
import { formatISODateString } from '../functions'
|
||||
import { addToast } from '../components/Toast/addToast'
|
||||
import { Offcanvas } from '../components/Offcanvas'
|
||||
import { EditAccount } from './EditAccount'
|
||||
|
||||
|
||||
const Home: FC = () => {
|
||||
const [rawd, setRawd] = useState<GetAccountResponse[]>([])
|
||||
const [editingAccount, setEditingAccount] = useState<GetAccountResponse | null>(
|
||||
null
|
||||
)
|
||||
const [accountId, setAccountId] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
console.log(GetApiRoute(ApiRoutes.ACCOUNTS).route)
|
||||
|
||||
getData<GetAccountResponse []>(GetApiRoute(ApiRoutes.ACCOUNTS).route).then((response) => {
|
||||
const loadData = useCallback(() => {
|
||||
getData<GetAccountResponse[]>(GetApiRoute(ApiRoutes.ACCOUNTS).route).then((response) => {
|
||||
if (!response) return
|
||||
|
||||
setRawd(response)
|
||||
})
|
||||
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [loadData])
|
||||
|
||||
const handleAccountUpdate = (updatedAccount: CacheAccount) => {
|
||||
// setAccounts(
|
||||
// accounts.map((account) =>
|
||||
@ -37,18 +36,36 @@ const Home: FC = () => {
|
||||
// )
|
||||
}
|
||||
|
||||
const deleteAccount = (accountId: string) => {
|
||||
const handleDeleteAccount = (accountId: string) => {
|
||||
deleteData<void>(
|
||||
GetApiRoute(ApiRoutes.ACCOUNT_DELETE)
|
||||
.route.replace('{accountId}', accountId)
|
||||
).then((result) => {
|
||||
if (!result) return
|
||||
|
||||
).then(_ => {
|
||||
setRawd(rawd.filter((account) => account.accountId !== accountId))
|
||||
})
|
||||
}
|
||||
|
||||
return <FormContainer>
|
||||
const handleEditCancel = () => {
|
||||
setAccountId(undefined)
|
||||
}
|
||||
|
||||
const handleRedeployCerts = (accountId: string) => {
|
||||
postData<void, { [key: string]: string }>(GetApiRoute(ApiRoutes.CERTS_FLOW_CERTIFICATES_APPLY).route
|
||||
.replace('{accountId}', accountId)
|
||||
).then(response => {
|
||||
if (!response?.message) return
|
||||
|
||||
addToast(response?.message, 'info')
|
||||
})
|
||||
}
|
||||
|
||||
const handleOnSubmitted = (_: GetAccountResponse) => {
|
||||
setAccountId(undefined)
|
||||
loadData()
|
||||
}
|
||||
|
||||
return <>
|
||||
<FormContainer>
|
||||
<FormHeader>Home</FormHeader>
|
||||
<FormContent>
|
||||
<div className={'grid grid-cols-12 gap-4 w-full'}>
|
||||
@ -59,18 +76,24 @@ const Home: FC = () => {
|
||||
rawd.map((acc) => (
|
||||
<div key={acc.accountId} className={'bg-white shadow-lg rounded-lg p-6 mb-6 col-span-12'}>
|
||||
<div className={'grid grid-cols-12 gap-4 w-full'}>
|
||||
<h2 className={'col-span-8'}>
|
||||
<h2 className={'col-span-6'}>
|
||||
Account: {acc.accountId}
|
||||
</h2>
|
||||
<ButtonComponent
|
||||
colspan={2}
|
||||
onClick={() => deleteAccount(acc.accountId)}
|
||||
label={'Delete'}
|
||||
onClick={() => handleDeleteAccount(acc.accountId)}
|
||||
label={'Delete Account'}
|
||||
buttonHierarchy={'error'}
|
||||
/>
|
||||
<ButtonComponent
|
||||
colspan={2}
|
||||
onClick={() => setEditingAccount(acc)}
|
||||
children={'Redeploy certs'}
|
||||
buttonHierarchy={'success'}
|
||||
onClick={() => handleRedeployCerts(acc.accountId)}
|
||||
/>
|
||||
<ButtonComponent
|
||||
colspan={2}
|
||||
onClick={() => setAccountId(acc.accountId)}
|
||||
label={'Edit'}
|
||||
/>
|
||||
<h3 className={'col-span-12'}>
|
||||
@ -97,7 +120,6 @@ const Home: FC = () => {
|
||||
{ value: 'staging', label: 'Staging' },
|
||||
{ value: 'production', label: 'Production' }
|
||||
]}
|
||||
|
||||
value={acc.challengeType ? 'staging' : 'production'}
|
||||
disabled={true}
|
||||
/>
|
||||
@ -106,7 +128,7 @@ const Home: FC = () => {
|
||||
{acc.hostnames?.map((hostname) => (
|
||||
<li key={hostname.hostname} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
|
||||
<span className={'col-span-3'}>{hostname.hostname}</span>
|
||||
<span className={'col-span-3'}>{formatISODateString(hostname.expires)}</span>
|
||||
<span className={'col-span-3'}>Exp: {formatISODateString(hostname.expires)}</span>
|
||||
<span className={'col-span-3'}>
|
||||
<span className={`${hostname.isUpcomingExpire
|
||||
? 'bg-yellow-200 text-yellow-800'
|
||||
@ -115,7 +137,6 @@ const Home: FC = () => {
|
||||
: 'Not Upcoming'}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<CheckBoxComponent
|
||||
colspan={3}
|
||||
value={hostname.isDisabled}
|
||||
@ -126,7 +147,6 @@ const Home: FC = () => {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<SelectBoxComponent
|
||||
label={'Environment'}
|
||||
options={[
|
||||
@ -139,16 +159,21 @@ const Home: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</FormContent>
|
||||
<FormFooter />
|
||||
</FormContainer>
|
||||
|
||||
<Offcanvas isOpen={accountId !== undefined}>
|
||||
{accountId && <EditAccount
|
||||
accountId={accountId}
|
||||
cancelEnabled={true}
|
||||
onSubmitted={handleOnSubmitted}
|
||||
onCancel={handleEditCancel}
|
||||
/>}
|
||||
|
||||
</Offcanvas>
|
||||
</>
|
||||
}
|
||||
|
||||
export { Home }
|
||||
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 { addToast } from '../components/Toast/addToast'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { PlusIcon, TrashIcon } from 'lucide-react'
|
||||
import { FieldContainer } from '../components/editors/FieldContainer'
|
||||
|
||||
|
||||
interface RegisterFormProps {
|
||||
@ -90,7 +92,7 @@ const Register: FC<RegisterProps> = () => {
|
||||
return
|
||||
}
|
||||
|
||||
postData<PostAccountRequest, GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_POST).route, request.data)
|
||||
postData<PostAccountRequest, GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_POST).route, request.data, 120000)
|
||||
.then(response => {
|
||||
if (!response) return
|
||||
|
||||
@ -115,14 +117,16 @@ const Register: FC<RegisterProps> = () => {
|
||||
{formState.contacts.map((contact) => (
|
||||
<li key={contact} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
|
||||
<span className={'col-span-10'}>{contact}</span>
|
||||
<FieldContainer colspan={2}>
|
||||
<ButtonComponent
|
||||
colspan={2}
|
||||
label={'TRASH'}
|
||||
onClick={() => {
|
||||
const updatedContacts = formState.contacts.filter(c => c !== contact)
|
||||
handleInputChange('contacts', updatedContacts)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<TrashIcon />
|
||||
</ButtonComponent>
|
||||
</FieldContainer>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -140,19 +144,23 @@ const Register: FC<RegisterProps> = () => {
|
||||
type={'text'}
|
||||
errorText={errors.contact}
|
||||
/>
|
||||
<FieldContainer colspan={2}>
|
||||
<ButtonComponent
|
||||
colspan={2}
|
||||
label={'PLUS'}
|
||||
onClick={() => {
|
||||
handleInputChange('contacts', [...formState.contacts, formState.contact])
|
||||
handleInputChange('contact', '')
|
||||
}}
|
||||
disabled={formState.contact.trim() === ''}
|
||||
/>
|
||||
>
|
||||
<PlusIcon />
|
||||
</ButtonComponent>
|
||||
</FieldContainer>
|
||||
<div className={'col-span-12'}>
|
||||
<SelectBoxComponent
|
||||
label={'Challenge Type'}
|
||||
options={enumToArr(ChallengeType).map(ct => ({ value: ct.value, label: ct.displayValue }))}
|
||||
options={enumToArr(ChallengeType)
|
||||
.map(ct => ({ value: ct.value, label: ct.displayValue }))
|
||||
.filter(ct => ct.value !== ChallengeType.dns01)}
|
||||
value={formState.challengeType}
|
||||
placeholder={'Select Challenge Type'}
|
||||
onChange={(e) => handleInputChange('challengeType', e.target.value)}
|
||||
@ -164,14 +172,16 @@ const Register: FC<RegisterProps> = () => {
|
||||
{formState.hostnames.map((hostname) => (
|
||||
<li key={hostname} className={'grid grid-cols-12 gap-4 w-full'}>
|
||||
<span className={'col-span-10'}>{hostname}</span>
|
||||
<FieldContainer colspan={2}>
|
||||
<ButtonComponent
|
||||
colspan={2}
|
||||
label={'TRASH'}
|
||||
onClick={() => {
|
||||
const updatedHostnames = formState.hostnames.filter(h => h !== hostname)
|
||||
handleInputChange('hostnames', updatedHostnames)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<TrashIcon />
|
||||
</ButtonComponent>
|
||||
</FieldContainer>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -189,15 +199,17 @@ const Register: FC<RegisterProps> = () => {
|
||||
type={'text'}
|
||||
errorText={errors.hostname}
|
||||
/>
|
||||
<FieldContainer colspan={2}>
|
||||
<ButtonComponent
|
||||
colspan={2}
|
||||
label={'PLUS'}
|
||||
onClick={() => {
|
||||
handleInputChange('hostnames', [...formState.hostnames, formState.hostname])
|
||||
handleInputChange('hostname', '')
|
||||
}}
|
||||
disabled={formState.hostname.trim() === ''}
|
||||
/>
|
||||
>
|
||||
<PlusIcon />
|
||||
</ButtonComponent>
|
||||
</FieldContainer>
|
||||
<RadioGroupComponent
|
||||
colspan={12}
|
||||
label={'LetsEncrypt Environment'}
|
||||
|
||||
@ -1,11 +1,23 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout'
|
||||
import { ButtonComponent, FileUploadComponent } from '../components/editors'
|
||||
import { ButtonComponent, DateTimePickerComponent, FileUploadComponent } from '../components/editors'
|
||||
import { ApiRoutes, GetApiRoute } from '../AppMap'
|
||||
import { getData } from '../axiosConfig'
|
||||
import { addToast } from '../components/Toast/addToast'
|
||||
|
||||
const Utilities: FC = () => {
|
||||
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
|
||||
const hadnleTestAgent = () => {
|
||||
getData(GetApiRoute(ApiRoutes.AGENT_TEST).route)
|
||||
.then((response) => {
|
||||
if (!response) return
|
||||
|
||||
addToast(response?.message, 'info')
|
||||
})
|
||||
}
|
||||
|
||||
return <FormContainer>
|
||||
<FormHeader>Utilities</FormHeader>
|
||||
<FormContent>
|
||||
@ -13,15 +25,8 @@ const Utilities: FC = () => {
|
||||
<ButtonComponent
|
||||
colspan={3}
|
||||
label={'Test agent'}
|
||||
buttonHierarchy={'primary'}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
|
||||
<ButtonComponent
|
||||
colspan={3}
|
||||
label={'Download cache files'}
|
||||
buttonHierarchy={'secondary'}
|
||||
onClick={() => {}}
|
||||
buttonHierarchy={'warning'}
|
||||
onClick={hadnleTestAgent}
|
||||
/>
|
||||
|
||||
<FileUploadComponent
|
||||
@ -31,9 +36,20 @@ const Utilities: FC = () => {
|
||||
onChange={setFiles}
|
||||
/>
|
||||
|
||||
<span className={'col-span-12'}></span>
|
||||
</div>
|
||||
<ButtonComponent
|
||||
colspan={3}
|
||||
children={'Download cache files'}
|
||||
buttonHierarchy={'secondary'}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
|
||||
<ButtonComponent
|
||||
colspan={3}
|
||||
children={'Destroy cache files'}
|
||||
buttonHierarchy={'error'}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</FormContent>
|
||||
<FormFooter />
|
||||
</FormContainer>
|
||||
|
||||
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'
|
||||
import { deepEqualArrays, deepEqual } from './deepEqual'
|
||||
import { deepMerge } from './deepMerge'
|
||||
|
||||
import { deepPatternMatch } from './deepPatternMatch'
|
||||
|
||||
|
||||
export {
|
||||
@ -14,5 +14,6 @@ export {
|
||||
deltaHasOperations,
|
||||
deepEqualArrays,
|
||||
deepEqual,
|
||||
deepMerge
|
||||
deepMerge,
|
||||
deepPatternMatch
|
||||
}
|
||||
@ -9,6 +9,7 @@ import {
|
||||
deltaHasOperations,
|
||||
deepEqual,
|
||||
deepMerge,
|
||||
deepPatternMatch
|
||||
} from './deep'
|
||||
|
||||
import {
|
||||
@ -39,6 +40,7 @@ export {
|
||||
deltaHasOperations,
|
||||
deepEqual,
|
||||
deepMerge,
|
||||
deepPatternMatch,
|
||||
|
||||
enumToArr,
|
||||
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