(refactor): webapi cache controller refactoring

This commit is contained in:
Maksym Sadovnychyy 2024-06-23 14:22:46 +02:00
parent bd67ad6a6b
commit 4000026b7a
35 changed files with 542 additions and 369 deletions

View File

@ -1,7 +1,7 @@
{
"editor.tabSize": 2,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"

View File

@ -1,9 +1,9 @@
enum ApiRoutes {
CACHE_ACCOUNTS = 'api/cache/accounts',
CACHE_ACCOUNT = 'api/cache/account/{accountId}',
CACHE_ACCOUNT_CONTACTS = 'api/cache/account/{accountId}/contacts',
CACHE_ACCOUNT_CONTACT = 'api/cache/account/{accountId}/contact/{index}',
CACHE_ACCOUNT_HOSTNAMES = 'api/cache/account/{accountId}/hostnames'
ACCOUNTS = 'api/accounts',
ACCOUNT = 'api/account/{accountId}',
ACCOUNT_CONTACTS = 'api/account/{accountId}/contacts',
ACCOUNT_CONTACT = 'api/account/{accountId}/contact/{index}',
ACCOUNT_HOSTNAMES = 'api/account/{accountId}/hostnames'
// CERTS_FLOW_CONFIGURE_CLIENT = `api/CertsFlow/ConfigureClient`,
// CERTS_FLOW_TERMS_OF_SERVICE = `api/CertsFlow/TermsOfService/{sessionId}`,

View File

@ -10,7 +10,7 @@ import {
} from '@/hooks/useValidation'
import { CustomButton, CustomInput } from '@/controls'
import { TrashIcon, PlusIcon } from '@heroicons/react/24/solid'
import { GetAccountResponse } from '@/models/letsEncryptServer/cache/responses/GetAccountResponse'
import { GetAccountResponse } from '@/models/letsEncryptServer/account/responses/GetAccountResponse'
import { deepCopy } from './functions'
import { CacheAccount } from '@/entities/CacheAccount'
@ -50,18 +50,21 @@ export default function Page() {
const fetchAccounts = async () => {
const newAccounts: CacheAccount[] = []
const accounts = await httpService.get<GetAccountResponse[]>(
GetApiRoute(ApiRoutes.CACHE_ACCOUNTS)
GetApiRoute(ApiRoutes.ACCOUNTS)
)
accounts?.forEach((account) => {
newAccounts.push({
accountId: account.accountId,
description: account.description,
contacts: account.contacts,
hostnames: account.hostnames.map((h) => ({
hostname: h.hostname,
expires: new Date(h.expires),
isUpcomingExpire: h.isUpcomingExpire
})),
challengeType: account.challengeType,
hostnames:
account.hostnames?.map((h) => ({
hostname: h.hostname,
expires: new Date(h.expires),
isUpcomingExpire: h.isUpcomingExpire
})) ?? [],
isEditMode: false
})
})
@ -97,7 +100,7 @@ export default function Page() {
// TODO: Remove from cache
httpService.delete(
GetApiRoute(ApiRoutes.CACHE_ACCOUNT_CONTACT, accountId, contact)
GetApiRoute(ApiRoutes.ACCOUNT_CONTACT, accountId, contact)
)
setAccounts(
@ -387,7 +390,9 @@ export default function Page() {
) : (
<>
<div className="mb-4">
<h3 className="text-xl font-medium mb-2">Description:</h3>
<h3 className="text-xl font-medium mb-2">
Description: {account.description}
</h3>
</div>
<div className="mb-4">
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
@ -399,6 +404,11 @@ export default function Page() {
))}
</ul>
</div>
<div className="mb-4">
<h3 className="text-xl font-medium mb-2">
Challenge type: {account.challengeType}
</h3>
</div>
<div>
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
<ul className="list-disc list-inside pl-4 mb-2">

View File

@ -4,6 +4,7 @@ export interface CacheAccount {
accountId: string
description?: string
contacts: string[]
challengeType?: string
hostnames: CacheAccountHostname[]
isEditMode: boolean
}

View File

@ -2,6 +2,8 @@ import { HostnameResponse } from './HostnameResponse'
export interface GetAccountResponse {
accountId: string
description?: string
contacts: string[]
hostnames: HostnameResponse[]
challengeType?: string
hostnames?: HostnameResponse[]
}

View File

@ -29,9 +29,10 @@ public class RegistrationCache {
/// <summary>
/// Field used to identify cache by account id
/// </summary>
public Guid AccountId { get; set; }
public required Guid AccountId { get; set; }
public string? Description { get; set; }
public string[]? Contacts { get; set; }
public required string[] Contacts { get; set; }
public string? ChallengeType { get; set; }
#endregion

View File

@ -1,8 +1,13 @@
using System.Text;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Net.Http.Headers;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using DomainResults.Common;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Exceptions;
using MaksIT.Core.Extensions;
@ -10,9 +15,6 @@ using MaksIT.LetsEncrypt.Models.Responses;
using MaksIT.LetsEncrypt.Models.Interfaces;
using MaksIT.LetsEncrypt.Models.Requests;
using MaksIT.LetsEncrypt.Entities.Jws;
using DomainResults.Common;
using System.Net.Http.Headers;
using System.Security.Principal;
namespace MaksIT.LetsEncrypt.Services;
@ -230,6 +232,7 @@ public class LetsEncryptService : ILetsEncryptService {
var challenge = challengeResponse.Result.Challenges.First(x => x.Type == challengeType);
state.Challenges.Add(challenge);
state.Cache.ChallengeType = challengeType;
var keyToken = state.JwsService.GetKeyAuthorization(challenge.Token);

View File

@ -12,7 +12,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
private readonly IOptions<Configuration> _appSettings;
private readonly ILogger<AutoRenewal> _logger;
private readonly ICacheInternalService _cacheService;
private readonly ICacheService _cacheService;
private readonly ICertsInternalService _certsFlowService;
public AutoRenewal(
@ -61,7 +61,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
return IDomainResult.Success();
}
var renewResult = await RenewCertificatesForHostnames(cache.AccountId, cache.Description, cache.Contacts, hostnames);
var renewResult = await RenewCertificatesForHostnames(cache.AccountId, cache.Description, cache.Contacts, hostnames, cache.ChallengeType);
if (!renewResult.IsSuccess)
return renewResult;
@ -70,7 +70,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
return IDomainResult.Success();
}
private async Task<IDomainResult> RenewCertificatesForHostnames(Guid accountId, string description, string[] contacts, string[] hostnames) {
private async Task<IDomainResult> RenewCertificatesForHostnames(Guid accountId, string description, string[] contacts, string[] hostnames, string challengeType) {
var (sessionId, configureClientResult) = await _certsFlowService.ConfigureClientAsync();
if (!configureClientResult.IsSuccess || sessionId == null) {
LogErrors(configureClientResult.Errors);
@ -85,7 +85,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
return initResult;
}
var (_, newOrderResult) = await _certsFlowService.NewOrderAsync(sessionIdValue, hostnames, "http-01");
var (_, newOrderResult) = await _certsFlowService.NewOrderAsync(sessionIdValue, hostnames, challengeType);
if (!newOrderResult.IsSuccess) {
LogErrors(newOrderResult.Errors);
return newOrderResult;

View File

@ -3,89 +3,128 @@
using DomainResults.Mvc;
using MaksIT.LetsEncryptServer.Services;
using MaksIT.Models.LetsEncryptServer.Cache.Requests;
using MaksIT.Models.LetsEncryptServer.Account.Requests;
namespace MaksIT.LetsEncryptServer.Controllers;
[ApiController]
[Route("api/account")]
[Route("api")]
public class AccountController : ControllerBase {
private readonly ICacheRestService _cacheService;
private readonly ICertsFlowService _certsFlowService;
private readonly IAccountRestService _accountService;
public AccountController(
ICacheService cacheService,
ICertsFlowService certsFlowService
IAccountService accountService
) {
_cacheService = cacheService;
_certsFlowService = certsFlowService;
_accountService = accountService;
}
#region Accounts
[HttpPost]
[HttpGet("accounts")]
public async Task<IActionResult> GetAccounts() {
var result = await _accountService.GetAccountsAsync();
return result.ToActionResult();
}
#endregion
#region Account
[HttpPost("account")]
public async Task<IActionResult> PostAccount([FromBody] PostAccountRequest requestData) {
//var result = await _cacheService.PostAccountAsync(requestData);
var result = await _accountService.PostAccountAsync(requestData);
return result.ToActionResult();
}
[HttpPut("account/{accountId:guid}")]
public async Task<IActionResult> PutAccount(Guid accountId, [FromBody] PutAccountRequest requestData) {
var result = await _accountService.PutAccountAsync(accountId, requestData);
return result.ToActionResult();
}
[HttpPatch("account/{accountId:guid}")]
public async Task<IActionResult> PatchAccount(Guid accountId, [FromBody] PatchAccountRequest requestData) {
var result = await _accountService.PatchAccountAsync(accountId, requestData);
return result.ToActionResult();
}
[HttpDelete("account/{accountd:guid}")]
public async Task<IActionResult> DeleteAccount(Guid accountId) {
var result = await _accountService.DeleteAccountAsync(accountId);
return result.ToActionResult();
}
#endregion
#region Account Contacts
[HttpGet("account/{accountId:guid}/contacts")]
public async Task<IActionResult> GetContacts(Guid accountId) {
var result = await _accountService.GetContactsAsync(accountId);
return result.ToActionResult();
}
[HttpPost("account/{accountId:guid}/contacts")]
public async Task<IActionResult> PostContacts(Guid accountId, [FromBody] PostContactsRequest requestData) {
//var result = await _accountService.PostContactsAsync(accountId, requestData);
//return result.ToActionResult();
return BadRequest("Not implemented");
}
[HttpPut("{accountId:guid}")]
public async Task<IActionResult> PutAccount(Guid accountId, [FromBody] PutAccountRequest requestData) {
var result = await _cacheService.PutAccountAsync(accountId, requestData);
return result.ToActionResult();
}
[HttpPatch("{accountId:guid}")]
public async Task<IActionResult> PatchAccount(Guid accountId, [FromBody] PatchAccountRequest requestData) {
var result = await _cacheService.PatchAccountAsync(accountId, requestData);
return result.ToActionResult();
}
[HttpDelete("{accountd:guid}")]
public async Task<IActionResult> DeleteAccount(Guid accountId) {
var result = await _cacheService.DeleteAccountAsync(accountId);
return result.ToActionResult();
}
#region Contacts
[HttpGet("{accountId:guid}/contacts")]
public async Task<IActionResult> GetContacts(Guid accountId) {
var result = await _cacheService.GetContactsAsync(accountId);
return result.ToActionResult();
}
[HttpPut("{accountId:guid}/contacts")]
[HttpPut("account/{accountId:guid}/contacts")]
public async Task<IActionResult> PutContacts(Guid accountId, [FromBody] PutContactsRequest requestData) {
var result = await _cacheService.PutContactsAsync(accountId, requestData);
var result = await _accountService.PutContactsAsync(accountId, requestData);
return result.ToActionResult();
}
[HttpPatch("{accountId:guid}/contacts")]
[HttpPatch("account/{accountId:guid}/contacts")]
public async Task<IActionResult> PatchContacts(Guid accountId, [FromBody] PatchContactsRequest requestData) {
var result = await _cacheService.PatchContactsAsync(accountId, requestData);
var result = await _accountService.PatchContactsAsync(accountId, requestData);
return result.ToActionResult();
}
[HttpDelete("{accountId:guid}/contact/{index:int}")]
[HttpDelete("account/{accountId:guid}/contact/{index:int}")]
public async Task<IActionResult> DeleteContact(Guid accountId, int index) {
var result = await _cacheService.DeleteContactAsync(accountId, index);
var result = await _accountService.DeleteContactAsync(accountId, index);
return result.ToActionResult();
}
#endregion
#region Hostnames
#region Account Hostnames
[HttpGet("{accountId:guid}/hostnames")]
public async Task<IActionResult> GetHostnames(Guid accountId) {
var result = await _cacheService.GetHostnames(accountId);
var result = await _accountService.GetHostnames(accountId);
return result.ToActionResult();
}
[HttpPost("{accountId:guid}")]
[HttpPost("account/{accountId:guid}/hostnames")]
public async Task<IActionResult> PostHostname(Guid accountId, [FromBody] PostHostnamesRequest requestData) {
//var result = await _cacheService.PostHostnameAsync(accountId, requestData);
//return result.ToActionResult();
[HttpDelete("{accountId:guid}/hostname/{index:int}")]
return BadRequest("Not implemented");
}
[HttpPut("account/{accountId:guid}/hostnames")]
public async Task<IActionResult> PutHostname(Guid accountId, [FromBody] PutHostnamesRequest requestData) {
//var result = await _cacheService.PutHostnameAsync(accountId, requestData);
//return result.ToActionResult();
return BadRequest("Not implemented");
}
[HttpPatch("account/{accountId:guid}/hostnames")]
public async Task<IActionResult> PatchHostname(Guid accountId, [FromBody] PatchHostnamesRequest requestData) {
//var result = await _cacheService.PatchHostnameAsync(accountId, requestData);
//return result.ToActionResult();
return BadRequest("Not implemented");
}
[HttpDelete("account/{accountId:guid}/hostname/{index:int}")]
public async Task<IActionResult> DeleteHostname(Guid accountId, int index) {
//var result = await _cacheService.DeleteHostnameAsync(accountId, index);
//return result.ToActionResult();

View File

@ -1,29 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using DomainResults.Mvc;
using MaksIT.LetsEncryptServer.Services;
using MaksIT.Models.LetsEncryptServer.Cache.Requests;
namespace MaksIT.LetsEncryptServer.Controllers;
[ApiController]
[Route("api/accounts")]
public class AccountsController : ControllerBase {
private readonly ICacheRestService _cacheService;
private readonly ICertsFlowService _certsFlowService;
public AccountsController(
ICacheService cacheService,
ICertsFlowService certsFlowService
) {
_cacheService = cacheService;
_certsFlowService = certsFlowService;
}
[HttpGet]
public async Task<IActionResult> GetAccounts() {
var result = await _cacheService.GetAccountsAsync();
return result.ToActionResult();
}
}

View File

@ -29,8 +29,9 @@ builder.Services.AddCors();
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<ILetsEncryptService, LetsEncryptService>();
builder.Services.AddSingleton<ICertsFlowService, CertsFlowService>();
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddSingleton<ICertsFlowService, CertsFlowService>();
builder.Services.AddSingleton<IAccountService, AccountService>();
builder.Services.AddHttpClient<IAgentService, AgentService>();
builder.Services.AddHostedService<AutoRenewal>();

View File

@ -0,0 +1,337 @@
using System.Text.Json;
using DomainResults.Common;
using MaksIT.Core.Extensions;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Models.Responses;
using MaksIT.Models;
using MaksIT.Models.LetsEncryptServer.Account.Requests;
using MaksIT.Models.LetsEncryptServer.Account.Responses;
namespace MaksIT.LetsEncryptServer.Services;
public interface IAccountInternalService {
}
public interface IAccountRestService {
Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync();
Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId);
Task<(GetAccountResponse?, IDomainResult)> PostAccountAsync(PostAccountRequest requestData);
Task<(GetAccountResponse?, IDomainResult)> PutAccountAsync(Guid accountId, PutAccountRequest requestData);
Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData);
Task<IDomainResult> DeleteAccountAsync(Guid accountId);
Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId);
Task<(GetContactsResponse?, IDomainResult)> PostContactsAsync(Guid accountId, PostContactsRequest requestData);
Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData);
Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactsRequest requestData);
Task<IDomainResult> DeleteContactAsync(Guid accountId, int index);
Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
}
public interface IAccountService : IAccountInternalService, IAccountRestService { }
public class AccountService : IAccountService {
private readonly ILogger<CacheService> _logger;
private readonly ICacheService _cacheService;
private readonly ICertsInternalService _certsFlowService;
public AccountService(
ILogger<CacheService> logger,
ICacheService cacheService,
ICertsFlowService certsFlowService
) {
_logger = logger;
_cacheService = cacheService;
_certsFlowService = certsFlowService;
}
#region Accounts
public async Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync() {
var (caches, result) = await _cacheService.LoadAccountsFromCacheAsync();
if (!result.IsSuccess || caches == null) {
return (null, result);
}
var accounts = caches.Select(cache => new GetAccountResponse {
AccountId = cache.AccountId,
Description = cache.Description,
Contacts = cache.Contacts,
ChallengeType = cache.ChallengeType,
Hostnames = GetHostnamesFromCache(cache).ToArray()
});
return IDomainResult.Success(accounts.ToArray());
}
public async Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId) {
var (cache, result) = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!result.IsSuccess || cache == null) {
return (null, result);
}
var response = new GetAccountResponse {
AccountId = accountId,
Description = cache.Description,
Contacts = cache.Contacts,
Hostnames = GetHostnamesFromCache(cache).ToArray()
};
return IDomainResult.Success(response);
}
public async Task<(GetAccountResponse?, IDomainResult)> PostAccountAsync(PostAccountRequest requestData) {
var (sessionId, configureClientResult) = await _certsFlowService.ConfigureClientAsync();
if (!configureClientResult.IsSuccess || sessionId == null) {
//LogErrors(configureClientResult.Errors);
return (null, configureClientResult);
}
var sessionIdValue = sessionId.Value;
var (_, initResult) = await _certsFlowService.InitAsync(sessionIdValue, null, requestData.Description, requestData.Contacts);
if (!initResult.IsSuccess) {
//LogErrors(initResult.Errors);
return (null, initResult);
}
var (_, newOrderResult) = await _certsFlowService.NewOrderAsync(sessionIdValue, requestData.Hostnames, requestData.ChallengeType);
if (!newOrderResult.IsSuccess) {
//LogErrors(newOrderResult.Errors);
return (null, newOrderResult);
}
var challengeResult = await _certsFlowService.CompleteChallengesAsync(sessionIdValue);
if (!challengeResult.IsSuccess) {
//LogErrors(challengeResult.Errors);
return (null, challengeResult);
}
var getOrderResult = await _certsFlowService.GetOrderAsync(sessionIdValue, requestData.Hostnames);
if (!getOrderResult.IsSuccess) {
//LogErrors(getOrderResult.Errors);
return (null, getOrderResult);
}
var certs = await _certsFlowService.GetCertificatesAsync(sessionIdValue, requestData.Hostnames);
if (!certs.IsSuccess) {
//LogErrors(certs.Errors);
return (null, certs);
}
var (_, applyCertsResult) = await _certsFlowService.ApplyCertificatesAsync(sessionIdValue, requestData.Hostnames);
if (!applyCertsResult.IsSuccess) {
//LogErrors(applyCertsResult.Errors);
return (null, applyCertsResult);
}
return IDomainResult.Success<GetAccountResponse?>(null);
}
public async Task<(GetAccountResponse?, IDomainResult)> PutAccountAsync(Guid accountId, PutAccountRequest requestData) {
var (cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
cache.Description = requestData.Description;
cache.Contacts = requestData.Contacts;
var saveResult = await _cacheService.SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) {
return (null, saveResult);
}
return CreateGetAccountResponse(accountId, cache);
}
public async Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
var (cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
if (requestData.Description != null) {
switch (requestData.Description.Op) {
case PatchOperation.Replace:
cache.Description = requestData.Description.Value;
break;
}
}
if (requestData.Contacts != null && requestData.Contacts.Any()) {
var contacts = cache.Contacts?.ToList() ?? new List<string>();
foreach (var action in requestData.Contacts) {
switch (action.Op)
{
case PatchOperation.Add:
if (action.Value != null) contacts.Add(action.Value);
break;
case PatchOperation.Replace:
if (action.Index != null && action.Index >= 0 && action.Index < contacts.Count)
contacts[action.Index.Value] = action.Value;
break;
case PatchOperation.Remove:
if (action.Index != null && action.Index >= 0 && action.Index < contacts.Count)
contacts.RemoveAt(action.Index.Value);
break;
}
}
cache.Contacts = contacts.ToArray();
}
var saveResult = await _cacheService.SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) {
return (null, saveResult);
}
return CreateGetAccountResponse(accountId, cache);
}
public async Task<IDomainResult> DeleteAccountAsync(Guid accountId) {
return await _cacheService.DeleteFromCacheAsync(accountId);
}
#endregion
#region Contacts Operations
public async Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId) {
var (cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
return IDomainResult.Success(new GetContactsResponse {
Contacts = cache.Contacts ?? Array.Empty<string>()
});
}
public async Task<(GetContactsResponse?, IDomainResult)> PostContactsAsync(Guid accountId, PostContactsRequest requestData) {
return IDomainResult.Failed<GetContactsResponse?>("Not implemented");
}
public async Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData) {
var (cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
cache.Contacts = requestData.Contacts;
var saveResult = await _cacheService.SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) {
return (null, saveResult);
}
return CreateGetAccountResponse(accountId, cache);
}
public async Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactsRequest requestData) {
var (cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
var contacts = cache.Contacts?.ToList() ?? new List<string>();
foreach (var contact in requestData.Contacts) {
switch (contact.Op) {
case PatchOperation.Add:
if (contact.Value != null) {
contacts.Add(contact.Value);
}
break;
case PatchOperation.Replace:
if (contact.Index.HasValue && contact.Index.Value >= 0 && contact.Index.Value < contacts.Count && contact.Value != null) {
contacts[contact.Index.Value] = contact.Value;
}
break;
case PatchOperation.Remove:
if (contact.Index.HasValue && contact.Index.Value >= 0 && contact.Index.Value < contacts.Count) {
contacts.RemoveAt(contact.Index.Value);
}
break;
default:
return (null, IDomainResult.Failed("Invalid patch operation."));
}
}
cache.Contacts = contacts.ToArray();
var saveResult = await _cacheService.SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) {
return (null, saveResult);
}
return CreateGetAccountResponse(accountId, cache);
}
public async Task<IDomainResult> DeleteContactAsync(Guid accountId, int index) {
var (cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return loadResult;
}
var contacts = cache.Contacts?.ToList() ?? new List<string>();
if (index >= 0 && index < contacts.Count) {
contacts.RemoveAt(index);
}
cache.Contacts = contacts.ToArray();
var saveResult = await _cacheService.SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) {
return saveResult;
}
return IDomainResult.Success();
}
#endregion
#region Hostnames Operations
public async Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId) {
var (cache, loadResult) = await _cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache?.CachedCerts == null) {
return (null, loadResult);
}
var hostnames = GetHostnamesFromCache(cache);
return IDomainResult.Success(new GetHostnamesResponse {
Hostnames = hostnames
});
}
private List<HostnameResponse> GetHostnamesFromCache(RegistrationCache cache) {
var hosts = cache.GetHosts().Select(x => new HostnameResponse {
Hostname = x.Hostname,
Expires = x.Expires,
IsUpcomingExpire = x.IsUpcomingExpire
}).ToList();
return hosts;
}
#endregion
#region Helper Methods
private (GetAccountResponse?, IDomainResult) CreateGetAccountResponse(Guid accountId, RegistrationCache cache) {
var hostnames = GetHostnamesFromCache(cache) ?? new List<HostnameResponse>();
return (new GetAccountResponse {
AccountId = accountId,
Description = cache.Description,
Contacts = cache.Contacts,
Hostnames = hostnames.ToArray()
}, IDomainResult.Success());
}
#endregion
}

View File

@ -5,33 +5,16 @@ using DomainResults.Common;
using MaksIT.Core.Extensions;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.Models;
using MaksIT.Models.LetsEncryptServer.Cache.Requests;
using MaksIT.Models.LetsEncryptServer.Cache.Responses;
namespace MaksIT.LetsEncryptServer.Services;
public interface ICacheInternalService {
public interface ICacheService {
Task<(RegistrationCache[]?, IDomainResult)> LoadAccountsFromCacheAsync();
Task<(RegistrationCache?, IDomainResult)> LoadAccountFromCacheAsync(Guid accountId);
Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
Task<IDomainResult> DeleteFromCacheAsync(Guid accountId);
}
public interface ICacheRestService {
Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync();
Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId);
Task<(GetAccountResponse?, IDomainResult)> PutAccountAsync(Guid accountId, PutAccountRequest requestData);
Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData);
Task<IDomainResult> DeleteAccountAsync(Guid accountId);
Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId);
Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData);
Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactsRequest requestData);
Task<IDomainResult> DeleteContactAsync(Guid accountId, int index);
Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
}
public interface ICacheService : ICacheInternalService, ICacheRestService {}
public class CacheService : ICacheService, IDisposable {
private readonly ILogger<CacheService> _logger;
private readonly string _cacheDirectory;
@ -86,9 +69,7 @@ public class CacheService : ICacheService, IDisposable {
}
public Task<(RegistrationCache?, IDomainResult)> LoadAccountFromCacheAsync(Guid accountId) {
return _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
}
private async Task<(RegistrationCache?, IDomainResult)> LoadFromCacheInternalAsync(Guid accountId) {
var cacheFilePath = GetCacheFilePath(accountId);
@ -110,9 +91,7 @@ public class CacheService : ICacheService, IDisposable {
return IDomainResult.Success(cache);
}
public Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
return _lockManager.ExecuteWithLockAsync(() => SaveToCacheInternalAsync(accountId, cache));
}
private async Task<IDomainResult> SaveToCacheInternalAsync(Guid accountId, RegistrationCache cache) {
var cacheFilePath = GetCacheFilePath(accountId);
@ -122,9 +101,7 @@ public class CacheService : ICacheService, IDisposable {
return DomainResult.Success();
}
public Task<IDomainResult> DeleteFromCacheAsync(Guid accountId) {
return _lockManager.ExecuteWithLockAsync(() => DeleteFromCacheInternal(accountId));
}
private IDomainResult DeleteFromCacheInternal(Guid accountId) {
var cacheFilePath = GetCacheFilePath(accountId);
@ -140,242 +117,20 @@ public class CacheService : ICacheService, IDisposable {
#endregion
#region Account Operations
public async Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync() {
return await _lockManager.ExecuteWithLockAsync(async () => {
var accountIds = GetCachedAccounts();
var accounts = new List<GetAccountResponse>();
foreach (var accountId in accountIds) {
var (account, result) = await GetAccountAsync(accountId);
if (!result.IsSuccess || account == null) {
return (null, result);
}
accounts.Add(account);
}
return IDomainResult.Success(accounts.ToArray());
});
public Task<(RegistrationCache?, IDomainResult)> LoadAccountFromCacheAsync(Guid accountId) {
return _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
}
public async Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId) {
return await _lockManager.ExecuteWithLockAsync(async () => {
var (cache, result) = await LoadAccountFromCacheAsync(accountId);
if (!result.IsSuccess || cache == null) {
return (null, result);
}
var response = new GetAccountResponse {
AccountId = accountId,
Description = cache.Description,
Contacts = cache.Contacts,
Hostnames = GetHostnamesFromCache(cache).ToArray()
};
return IDomainResult.Success(response);
});
public Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
return _lockManager.ExecuteWithLockAsync(() => SaveToCacheInternalAsync(accountId, cache));
}
public async Task<(GetAccountResponse?, IDomainResult)> PutAccountAsync(Guid accountId, PutAccountRequest requestData) {
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
cache.Description = requestData.Description;
cache.Contacts = requestData.Contacts;
var saveResult = await SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) {
return (null, saveResult);
}
return CreateGetAccountResponse(accountId, cache);
}
public async Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
if (requestData.Description != null) {
switch (requestData.Description.Op) {
case PatchOperation.Replace:
cache.Description = requestData.Description.Value;
break;
}
}
if (requestData.Contacts != null && requestData.Contacts.Any()) {
var contacts = cache.Contacts?.ToList() ?? new List<string>();
foreach (var action in requestData.Contacts) {
switch (action.Op)
{
case PatchOperation.Add:
if (action.Value != null) contacts.Add(action.Value);
break;
case PatchOperation.Replace:
if (action.Index != null && action.Index >= 0 && action.Index < contacts.Count)
contacts[action.Index.Value] = action.Value;
break;
case PatchOperation.Remove:
if (action.Index != null && action.Index >= 0 && action.Index < contacts.Count)
contacts.RemoveAt(action.Index.Value);
break;
}
}
cache.Contacts = contacts.ToArray();
}
var saveResult = await SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) {
return (null, saveResult);
}
return CreateGetAccountResponse(accountId, cache);
}
public async Task<IDomainResult> DeleteAccountAsync(Guid accountId) {
return await DeleteFromCacheAsync(accountId);
}
#endregion
#region Contacts Operations
public async Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId) {
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
return IDomainResult.Success(new GetContactsResponse {
Contacts = cache.Contacts ?? Array.Empty<string>()
});
}
public async Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData) {
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
cache.Contacts = requestData.Contacts;
var saveResult = await SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) {
return (null, saveResult);
}
return CreateGetAccountResponse(accountId, cache);
}
public async Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactsRequest requestData) {
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return (null, loadResult);
}
var contacts = cache.Contacts?.ToList() ?? new List<string>();
foreach (var contact in requestData.Contacts) {
switch (contact.Op) {
case PatchOperation.Add:
if (contact.Value != null) {
contacts.Add(contact.Value);
}
break;
case PatchOperation.Replace:
if (contact.Index.HasValue && contact.Index.Value >= 0 && contact.Index.Value < contacts.Count && contact.Value != null) {
contacts[contact.Index.Value] = contact.Value;
}
break;
case PatchOperation.Remove:
if (contact.Index.HasValue && contact.Index.Value >= 0 && contact.Index.Value < contacts.Count) {
contacts.RemoveAt(contact.Index.Value);
}
break;
default:
return (null, IDomainResult.Failed("Invalid patch operation."));
}
}
cache.Contacts = contacts.ToArray();
var saveResult = await SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) {
return (null, saveResult);
}
return CreateGetAccountResponse(accountId, cache);
}
public async Task<IDomainResult> DeleteContactAsync(Guid accountId, int index) {
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null) {
return loadResult;
}
var contacts = cache.Contacts?.ToList() ?? new List<string>();
if (index >= 0 && index < contacts.Count) {
contacts.RemoveAt(index);
}
cache.Contacts = contacts.ToArray();
var saveResult = await SaveToCacheAsync(accountId, cache);
if (!saveResult.IsSuccess) {
return saveResult;
}
return IDomainResult.Success();
}
#endregion
#region Hostnames Operations
public async Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId) {
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache?.CachedCerts == null) {
return (null, loadResult);
}
var hostnames = GetHostnamesFromCache(cache);
return IDomainResult.Success(new GetHostnamesResponse {
Hostnames = hostnames
});
}
private List<HostnameResponse> GetHostnamesFromCache(RegistrationCache cache) {
var hosts = cache.GetHosts().Select(x => new HostnameResponse {
Hostname = x.Hostname,
Expires = x.Expires,
IsUpcomingExpire = x.IsUpcomingExpire
}).ToList();
return hosts;
}
#endregion
#region Helper Methods
private (GetAccountResponse?, IDomainResult) CreateGetAccountResponse(Guid accountId, RegistrationCache cache) {
var hostnames = GetHostnamesFromCache(cache) ?? new List<HostnameResponse>();
return (new GetAccountResponse {
AccountId = accountId,
Description = cache.Description,
Contacts = cache.Contacts,
Hostnames = hostnames.ToArray()
}, IDomainResult.Success());
public Task<IDomainResult> DeleteFromCacheAsync(Guid accountId) {
return _lockManager.ExecuteWithLockAsync(() => DeleteFromCacheInternal(accountId));
}
public void Dispose() {
_lockManager?.Dispose();
_lockManager.Dispose();
}
#endregion
}

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
namespace MaksIT.Models.LetsEncryptServer.Account.Requests {
public class PatchAccountRequest {
public PatchAction<string>? Description { get; set; }

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
namespace MaksIT.Models.LetsEncryptServer.Account.Requests {
public class PatchContactsRequest {
public List<PatchAction<string>> Contacts { get; set; }

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Account.Requests {
public class PatchHostnamesRequest {
public List<PatchAction<string>> Hostnames { get; set; }
}
}

View File

@ -1,11 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
namespace MaksIT.Models.LetsEncryptServer.Account.Requests {
public class PostAccountRequest : IValidatableObject {
public required string Description { get; set; }
public required string[] Contacts { get; set; }
public required string[] Hostnames { get; set; }
public required string ChallengeType { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (string.IsNullOrWhiteSpace(Description))
yield return new ValidationResult("Description is required", new[] { nameof(Description) });
@ -15,6 +17,9 @@ namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
if (string.IsNullOrWhiteSpace(ChallengeType) && ChallengeType != "http-01")
yield return new ValidationResult("ChallengeType is required", new[] { nameof(ChallengeType) });
}
}
}

View File

@ -5,7 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
namespace MaksIT.Models.LetsEncryptServer.Account.Requests {
public class PostContactsRequest : IValidatableObject {
public required string[] Contacts { get; set; }

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Account.Requests {
public class PostHostnamesRequest : IValidatableObject {
public required string[] Hostnames { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
}
}
}

View File

@ -5,7 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
namespace MaksIT.Models.LetsEncryptServer.Account.Requests {
public class PutAccountRequest : IValidatableObject {
public required string Description { get; set; }
public required string[] Contacts { get; set; }

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
namespace MaksIT.Models.LetsEncryptServer.Account.Requests {
public class PutContactsRequest : IValidatableObject {
public required string[] Contacts { get; set; }

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Account.Requests {
public class PutHostnamesRequest : IValidatableObject {
public string[] Hostnames { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
}
}
}

View File

@ -4,13 +4,15 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
namespace MaksIT.Models.LetsEncryptServer.Account.Responses {
public class GetAccountResponse {
public Guid AccountId { get; set; }
public string? Description { get; set; }
public string []? Contacts { get; set; }
public required string [] Contacts { get; set; }
public string? ChallengeType { get; set; }
public HostnameResponse[]? Hostnames { get; set; }
}

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
namespace MaksIT.Models.LetsEncryptServer.Account.Responses {
public class GetContactsResponse {
public string[] Contacts { get; set; }
}

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
namespace MaksIT.Models.LetsEncryptServer.Account.Responses {
public class GetHostnamesResponse {
public List<HostnameResponse> Hostnames { get; set; }

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
namespace MaksIT.Models.LetsEncryptServer.Account.Responses {
public class HostnameResponse {
public string Hostname { get; set; }
public DateTime Expires { get; set; }

File diff suppressed because one or more lines are too long