) => {
+ onChange?.(e.target.value);
+ }
+
return (
{title && }
onChange(e.target.value)}
+ onChange={handleChange}
placeholder={placeholder}
className={inputClassName}
/>
diff --git a/src/ClientApp/controls/index.ts b/src/ClientApp/controls/index.ts
index 43b80c5..18bbd54 100644
--- a/src/ClientApp/controls/index.ts
+++ b/src/ClientApp/controls/index.ts
@@ -1,5 +1,5 @@
-import { CustomButton } from "./customButton"
-import { CustomInput } from "./customInput"
+import { CustomButton } from "./customButton";
+import { CustomInput } from "./customInput";
export {
CustomButton,
diff --git a/src/ClientApp/hooks/useValidation.tsx b/src/ClientApp/hooks/useValidation.tsx
index 4238a89..b0f6582 100644
--- a/src/ClientApp/hooks/useValidation.tsx
+++ b/src/ClientApp/hooks/useValidation.tsx
@@ -6,17 +6,35 @@ const isValidEmail = (email: string) => {
return emailRegex.test(email)
}
+const isValidPhoneNumber = (phone: string) => {
+ const phoneRegex = /^\+?[1-9]\d{1,14}$/
+ return phoneRegex.test(phone)
+}
+
+const isValidContact = (contact: string) => {
+ return isValidEmail(contact) || isValidPhoneNumber(contact)
+}
+
const isValidHostname = (hostname: string) => {
const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/
return hostnameRegex.test(hostname)
}
+// Props interface for useValidation hook
+interface UseValidationProps {
+ initialValue: string
+ validateFn: (value: string) => boolean
+ errorMessage: string
+}
+
// Custom hook for input validation
-const useValidation = (initialValue: string, validateFn: (value: string) => boolean, errorMessage: string) => {
+const useValidation = ({ initialValue, validateFn, errorMessage }: UseValidationProps) => {
const [value, setValue] = useState(initialValue)
const [error, setError] = useState("")
const handleChange = (newValue: string) => {
+
+ console.log(newValue)
setValue(newValue)
if (newValue.trim() === "") {
setError("This field cannot be empty.")
@@ -31,7 +49,7 @@ const useValidation = (initialValue: string, validateFn: (value: string) => bool
handleChange(initialValue)
}, [initialValue])
- return { value, error, handleChange }
+ return { value, error, handleChange, reset: () => setValue("") }
}
-export { useValidation, isValidEmail, isValidHostname }
+export { useValidation, isValidEmail, isValidPhoneNumber, isValidContact, isValidHostname }
diff --git a/src/ClientApp/models/letsEncryptServer/cache/GetAccountsResponse.ts b/src/ClientApp/models/letsEncryptServer/cache/GetAccountsResponse.ts
deleted file mode 100644
index 65f6566..0000000
--- a/src/ClientApp/models/letsEncryptServer/cache/GetAccountsResponse.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export interface GetAccountsResponse {
- accountIds: string[]
-}
\ No newline at end of file
diff --git a/src/ClientApp/models/letsEncryptServer/cache/GetHostnamesResponse.ts b/src/ClientApp/models/letsEncryptServer/cache/GetHostnamesResponse.ts
deleted file mode 100644
index da07b14..0000000
--- a/src/ClientApp/models/letsEncryptServer/cache/GetHostnamesResponse.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-interface HostnameResponse {
- hostname: string
- expires: string,
- isUpcomingExpire: boolean
-}
-
-export interface GetHostnamesResponse {
- hostnames: HostnameResponse[]
-}
\ No newline at end of file
diff --git a/src/ClientApp/models/letsEncryptServer/cache/responses/GetAccountResponse.ts b/src/ClientApp/models/letsEncryptServer/cache/responses/GetAccountResponse.ts
new file mode 100644
index 0000000..240f996
--- /dev/null
+++ b/src/ClientApp/models/letsEncryptServer/cache/responses/GetAccountResponse.ts
@@ -0,0 +1,9 @@
+import { HostnameResponse } from "./HostnameResponse";
+
+export interface GetAccountResponse {
+ accountId: string,
+ contacts: string[],
+ hostnames: HostnameResponse[],
+}
+
+
diff --git a/src/ClientApp/models/letsEncryptServer/cache/GetContactsResponse.ts b/src/ClientApp/models/letsEncryptServer/cache/responses/GetContactsResponse.ts
similarity index 100%
rename from src/ClientApp/models/letsEncryptServer/cache/GetContactsResponse.ts
rename to src/ClientApp/models/letsEncryptServer/cache/responses/GetContactsResponse.ts
diff --git a/src/ClientApp/models/letsEncryptServer/cache/responses/GetHostnamesResponse.ts b/src/ClientApp/models/letsEncryptServer/cache/responses/GetHostnamesResponse.ts
new file mode 100644
index 0000000..459354a
--- /dev/null
+++ b/src/ClientApp/models/letsEncryptServer/cache/responses/GetHostnamesResponse.ts
@@ -0,0 +1,5 @@
+import { HostnameResponse } from "./HostnameResponse";
+
+export interface GetHostnamesResponse {
+ hostnames: HostnameResponse[]
+}
\ No newline at end of file
diff --git a/src/ClientApp/models/letsEncryptServer/cache/responses/HostnameResponse.ts b/src/ClientApp/models/letsEncryptServer/cache/responses/HostnameResponse.ts
new file mode 100644
index 0000000..b97e8ee
--- /dev/null
+++ b/src/ClientApp/models/letsEncryptServer/cache/responses/HostnameResponse.ts
@@ -0,0 +1,5 @@
+export interface HostnameResponse {
+ hostname: string
+ expires: string
+ isUpcomingExpire: boolean
+ }
\ No newline at end of file
diff --git a/src/Core/LockManager.cs b/src/Core/LockManager.cs
index c857ac7..a5f6119 100644
--- a/src/Core/LockManager.cs
+++ b/src/Core/LockManager.cs
@@ -1,55 +1,105 @@
using System;
+using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
public class LockManager : IDisposable {
- private readonly SemaphoreSlim _semaphore;
-
- public LockManager(int initialCount, int maxCount) {
- _semaphore = new SemaphoreSlim(initialCount, maxCount);
- }
+ private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
+ private readonly ConcurrentDictionary _reentrantCounts = new ConcurrentDictionary();
public async Task ExecuteWithLockAsync(Func> action) {
- await _semaphore.WaitAsync();
+ var threadId = Thread.CurrentThread.ManagedThreadId;
+
+ if (!_reentrantCounts.ContainsKey(threadId)) {
+ _reentrantCounts[threadId] = 0;
+ }
+
+ if (_reentrantCounts[threadId] == 0) {
+ await _semaphore.WaitAsync();
+ }
+
+ _reentrantCounts[threadId]++;
try {
return await action();
}
finally {
- _semaphore.Release();
+ _reentrantCounts[threadId]--;
+ if (_reentrantCounts[threadId] == 0) {
+ _semaphore.Release();
+ }
}
}
public async Task ExecuteWithLockAsync(Func action) {
- await _semaphore.WaitAsync();
+ var threadId = Thread.CurrentThread.ManagedThreadId;
+
+ if (!_reentrantCounts.ContainsKey(threadId)) {
+ _reentrantCounts[threadId] = 0;
+ }
+
+ if (_reentrantCounts[threadId] == 0) {
+ await _semaphore.WaitAsync();
+ }
+
+ _reentrantCounts[threadId]++;
try {
await action();
}
finally {
- _semaphore.Release();
+ _reentrantCounts[threadId]--;
+ if (_reentrantCounts[threadId] == 0) {
+ _semaphore.Release();
+ }
}
}
public async Task ExecuteWithLockAsync(Func action) {
- await _semaphore.WaitAsync();
+ var threadId = Thread.CurrentThread.ManagedThreadId;
+
+ if (!_reentrantCounts.ContainsKey(threadId)) {
+ _reentrantCounts[threadId] = 0;
+ }
+
+ if (_reentrantCounts[threadId] == 0) {
+ await _semaphore.WaitAsync();
+ }
+
+ _reentrantCounts[threadId]++;
try {
return await Task.Run(action);
}
finally {
- _semaphore.Release();
+ _reentrantCounts[threadId]--;
+ if (_reentrantCounts[threadId] == 0) {
+ _semaphore.Release();
+ }
}
}
public async Task ExecuteWithLockAsync(Action action) {
- await _semaphore.WaitAsync();
+ var threadId = Thread.CurrentThread.ManagedThreadId;
+
+ if (!_reentrantCounts.ContainsKey(threadId)) {
+ _reentrantCounts[threadId] = 0;
+ }
+
+ if (_reentrantCounts[threadId] == 0) {
+ await _semaphore.WaitAsync();
+ }
+
+ _reentrantCounts[threadId]++;
try {
await Task.Run(action);
}
finally {
- _semaphore.Release();
+ _reentrantCounts[threadId]--;
+ if (_reentrantCounts[threadId] == 0) {
+ _semaphore.Release();
+ }
}
}
public void Dispose() {
- _semaphore?.Dispose();
+ _semaphore.Dispose();
}
}
diff --git a/src/LetsEncryptServer/BackgroundServices/AutoRenewal.cs b/src/LetsEncryptServer/BackgroundServices/AutoRenewal.cs
index ae7d1cf..fc5eb33 100644
--- a/src/LetsEncryptServer/BackgroundServices/AutoRenewal.cs
+++ b/src/LetsEncryptServer/BackgroundServices/AutoRenewal.cs
@@ -5,6 +5,8 @@ using DomainResults.Common;
using MaksIT.LetsEncryptServer.Services;
using Models.LetsEncryptServer.CertsFlow.Requests;
+using Models.LetsEncryptServer.Cache.Responses;
+using MaksIT.LetsEncrypt.Entities;
namespace MaksIT.LetsEncryptServer.BackgroundServices {
public class AutoRenewal : BackgroundService {
@@ -30,26 +32,23 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
while (!stoppingToken.IsCancellationRequested) {
_logger.LogInformation("Background service is running.");
- var (accountsResponse, getAccountIdsResult) = await _cacheService.GetAccountsAsync();
+ var (accountsResponse, getAccountIdsResult) = await _cacheService.LoadAccountsFromCacheAsync();
if (!getAccountIdsResult.IsSuccess || accountsResponse == null) {
LogErrors(getAccountIdsResult.Errors);
continue;
}
- foreach (var accountId in accountsResponse.AccountIds) {
- await ProcessAccountAsync(accountId);
+ foreach (var account in accountsResponse) {
+ await ProcessAccountAsync(account);
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
- private async Task ProcessAccountAsync(Guid accountId) {
- var (cache, loadResult) = await _cacheService.LoadFromCacheAsync(accountId);
- if (!loadResult.IsSuccess || cache == null) {
- LogErrors(loadResult.Errors);
- return loadResult;
- }
+ private async Task ProcessAccountAsync(RegistrationCache cache) {
+
+
var hostnames = cache.GetHostsWithUpcomingSslExpiry();
if (hostnames == null) {
@@ -63,11 +62,11 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
return IDomainResult.Success();
}
- var renewResult = await RenewCertificatesForHostnames(accountId, cache.Contacts, hostnames);
+ var renewResult = await RenewCertificatesForHostnames(cache.AccountId, cache.Contacts, hostnames);
if (!renewResult.IsSuccess)
return renewResult;
- _logger.LogInformation($"Certificates renewed for account {accountId}");
+ _logger.LogInformation($"Certificates renewed for account {cache.AccountId}");
return IDomainResult.Success();
}
diff --git a/src/LetsEncryptServer/Controllers/CacheController.cs b/src/LetsEncryptServer/Controllers/CacheController.cs
index aa9756e..552ade3 100644
--- a/src/LetsEncryptServer/Controllers/CacheController.cs
+++ b/src/LetsEncryptServer/Controllers/CacheController.cs
@@ -11,15 +11,12 @@ namespace MaksIT.LetsEncryptServer.Controllers;
[ApiController]
[Route("api/cache")]
public class CacheController : ControllerBase {
- private readonly Configuration _appSettings;
private readonly ICacheRestService _cacheService;
public CacheController(
- IOptions appSettings,
ICacheService cacheService
) {
- _appSettings = appSettings.Value;
- _cacheService = (ICacheRestService)cacheService;
+ _cacheService = cacheService;
}
[HttpGet("accounts")]
diff --git a/src/LetsEncryptServer/Services/CacheService.cs b/src/LetsEncryptServer/Services/CacheService.cs
index 1a5cb98..8ae823f 100644
--- a/src/LetsEncryptServer/Services/CacheService.cs
+++ b/src/LetsEncryptServer/Services/CacheService.cs
@@ -10,8 +10,9 @@ using Models.LetsEncryptServer.Cache.Responses;
namespace MaksIT.LetsEncryptServer.Services;
-public interface ICacheService {
- Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId);
+public interface ICacheInternalsService {
+ Task<(RegistrationCache[]?, IDomainResult)> LoadAccountsFromCacheAsync();
+ Task<(RegistrationCache?, IDomainResult)> LoadAccountFromCacheAsync(Guid accountId);
Task SaveToCacheAsync(Guid accountId, RegistrationCache cache);
Task DeleteFromCacheAsync(Guid accountId);
}
@@ -28,7 +29,9 @@ public interface ICacheRestService {
Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
}
-public class CacheService : ICacheService, ICacheRestService, IDisposable {
+public interface ICacheService : ICacheInternalsService, ICacheRestService {}
+
+public class CacheService : ICacheService, IDisposable {
private readonly ILogger _logger;
private readonly string _cacheDirectory;
private readonly LockManager _lockManager;
@@ -36,7 +39,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
public CacheService(ILogger logger) {
_logger = logger;
_cacheDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cache");
- _lockManager = new LockManager(1, 1);
+ _lockManager = new LockManager();
if (!Directory.Exists(_cacheDirectory)) {
Directory.CreateDirectory(_cacheDirectory);
@@ -50,9 +53,39 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
return Path.Combine(_cacheDirectory, $"{accountId}.json");
}
+ private Guid[] GetCachedAccounts() {
+ return GetCacheFilesPaths().Select(x => Path.GetFileNameWithoutExtension(x).ToGuid()).Where(x => x != Guid.Empty).ToArray();
+ }
+
+ private string[] GetCacheFilesPaths() {
+ return Directory.GetFiles(_cacheDirectory);
+ }
+
#region Cache Operations
- public Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId) {
+ public async Task<(RegistrationCache[]?, IDomainResult)> LoadAccountsFromCacheAsync() {
+ return await _lockManager.ExecuteWithLockAsync(async () => {
+ var accountIds = GetCachedAccounts();
+ var cacheLoadTasks = accountIds.Select(accountId => LoadFromCacheInternalAsync(accountId)).ToList();
+
+ var caches = new List();
+ foreach (var task in cacheLoadTasks) {
+ var (registrationCache, getRegistrationCacheResult) = await task;
+ if (!getRegistrationCacheResult.IsSuccess || registrationCache == null) {
+ // Depending on how you want to handle partial failures, you might want to return here
+ // or continue loading other caches. For now, let's continue.
+ continue;
+ }
+
+ caches.Add(registrationCache);
+ }
+
+ return IDomainResult.Success(caches.ToArray());
+ });
+ }
+
+
+ public Task<(RegistrationCache?, IDomainResult)> LoadAccountFromCacheAsync(Guid accountId) {
return _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
}
@@ -110,8 +143,8 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
public async Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync() {
return await _lockManager.ExecuteWithLockAsync(async () => {
- var cacheFiles = Directory.GetFiles(_cacheDirectory);
- var accountIds = cacheFiles.Select(x => Path.GetFileNameWithoutExtension(x).ToGuid()).ToArray();
+
+ var accountIds = GetCachedAccounts();
var accounts = new List();
foreach (var accountId in accountIds) {
@@ -128,7 +161,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
public async Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId) {
return await _lockManager.ExecuteWithLockAsync(async () => {
- var (cache, result) = await LoadFromCacheAsync(accountId);
+ var (cache, result) = await LoadAccountFromCacheAsync(accountId);
if (!result.IsSuccess || cache == null) {
return (null, result);
}
@@ -145,7 +178,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
}
public async Task<(GetAccountResponse?, IDomainResult)> PutAccountAsync(Guid accountId, PutAccountRequest requestData) {
- var (cache, loadResult) = await LoadFromCacheAsync(accountId);
+ var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
@@ -162,7 +195,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
}
public async Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
- var (cache, loadResult) = await LoadFromCacheAsync(accountId);
+ var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
@@ -209,7 +242,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
#region Contacts Operations
public async Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId) {
- var (cache, loadResult) = await LoadFromCacheAsync(accountId);
+ var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
@@ -220,7 +253,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
}
public async Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData) {
- var (cache, loadResult) = await LoadFromCacheAsync(accountId);
+ var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
@@ -235,7 +268,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
}
public async Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactsRequest requestData) {
- var (cache, loadResult) = await LoadFromCacheAsync(accountId);
+ var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
@@ -274,7 +307,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
}
public async Task DeleteContactAsync(Guid accountId, int index) {
- var (cache, loadResult) = await LoadFromCacheAsync(accountId);
+ var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return loadResult;
}
@@ -299,7 +332,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
#region Hostnames Operations
public async Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId) {
- var (cache, loadResult) = await LoadFromCacheAsync(accountId);
+ var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache?.CachedCerts == null) {
return (null, loadResult);
}
diff --git a/src/LetsEncryptServer/Services/CertsFlowService.cs b/src/LetsEncryptServer/Services/CertsFlowService.cs
index e9a0007..7138ff7 100644
--- a/src/LetsEncryptServer/Services/CertsFlowService.cs
+++ b/src/LetsEncryptServer/Services/CertsFlowService.cs
@@ -83,7 +83,7 @@ public class CertsFlowService : ICertsFlowService {
accountId = Guid.NewGuid();
}
else {
- var (loadedCache, loadCaceResutl) = await _cacheService.LoadFromCacheAsync(accountId.Value);
+ var (loadedCache, loadCaceResutl) = await _cacheService.LoadAccountFromCacheAsync(accountId.Value);
if (!loadCaceResutl.IsSuccess || loadCaceResutl == null) {
accountId = Guid.NewGuid();
}