mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): frontend edit accounts form
This commit is contained in:
parent
2f87b455ca
commit
1f56ac19e6
@ -1,15 +1,30 @@
|
||||
enum ApiRoutes {
|
||||
|
||||
CACHE_GET_ACCOUNTS = `/api/Cache/GetAccounts`,
|
||||
CACHE_GET_CONTACTS = `/api/Cache/GetContacts/{accountId}`,
|
||||
CACHE_SET_CONTACTS = `/api/Cache/SetContacts/{accountId}`,
|
||||
CACHE_GET_ACCOUNTS = `api/Cache/GetAccounts`,
|
||||
CACHE_GET_CONTACTS = `api/Cache/GetContacts/{accountId}`,
|
||||
CACHE_SET_CONTACTS = `api/Cache/SetContacts/{accountId}`,
|
||||
CACHE_GET_HOSTNAMES = `api/Cache/GetHostnames/{accountId}`,
|
||||
|
||||
CERTS_FLOW_CONFIGURE_CLIENT = `/api/CertsFlow/ConfigureClient`,
|
||||
CERTS_FLOW_TERMS_OF_SERVICE = `/api/CertsFlow/TermsOfService/{sessionId}`,
|
||||
CERTS_FLOW_INIT = `/api/CertsFlow/Init/{sessionId}/{accountId}`,
|
||||
CERTS_FLOW_NEW_ORDER = `/api/CertsFlow/NewOrder/{sessionId}`,
|
||||
CERTS_FLOW_GET_ORDER = `/api/CertsFlow/GetOrder/{sessionId}`,
|
||||
CERTS_FLOW_GET_CERTIFICATES = `/api/CertsFlow/GetCertificates/{sessionId}`,
|
||||
CERTS_FLOW_APPLY_CERTIFICATES = `/api/CertsFlow/ApplyCertificates/{sessionId}`,
|
||||
CERTS_FLOW_HOSRS_WITH_UPCOMING_SSL_EXPIRY = `/api/CertsFlow/HostsWithUpcomingSslExpiry/{sessionId}`
|
||||
CERTS_FLOW_CONFIGURE_CLIENT = `api/CertsFlow/ConfigureClient`,
|
||||
CERTS_FLOW_TERMS_OF_SERVICE = `api/CertsFlow/TermsOfService/{sessionId}`,
|
||||
CERTS_FLOW_INIT = `api/CertsFlow/Init/{sessionId}/{accountId}`,
|
||||
CERTS_FLOW_NEW_ORDER = `api/CertsFlow/NewOrder/{sessionId}`,
|
||||
CERTS_FLOW_GET_ORDER = `api/CertsFlow/GetOrder/{sessionId}`,
|
||||
CERTS_FLOW_GET_CERTIFICATES = `api/CertsFlow/GetCertificates/{sessionId}`,
|
||||
CERTS_FLOW_APPLY_CERTIFICATES = `api/CertsFlow/ApplyCertificates/{sessionId}`,
|
||||
CERTS_FLOW_HOSRS_WITH_UPCOMING_SSL_EXPIRY = `api/CertsFlow/HostsWithUpcomingSslExpiry/{sessionId}`
|
||||
}
|
||||
|
||||
const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => {
|
||||
let result: string = route;
|
||||
args.forEach(arg => {
|
||||
result = result.replace(/{.*?}/, arg);
|
||||
});
|
||||
return 'http://localhost:5000/' + result;
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
GetApiRoute,
|
||||
ApiRoutes
|
||||
}
|
||||
@ -1,9 +1,228 @@
|
||||
"use client"; // Add this line
|
||||
|
||||
import { ApiRoutes, GetApiRoute } from "@/ApiRoutes";
|
||||
import { GetAccountsResponse } from "@/models/letsEncryptServer/cache/GetAccountsResponse";
|
||||
import { GetContactsResponse } from "@/models/letsEncryptServer/cache/GetContactsResponse";
|
||||
import { GetHostnamesResponse } from "@/models/letsEncryptServer/cache/GetHostnamesResponse";
|
||||
import { httpService } from "@/services/HttpService";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useValidation, isValidEmail, isValidHostname } from "@/hooks/useValidation"; // Assuming hooks are in a hooks directory
|
||||
|
||||
interface CacheAccountHostname {
|
||||
hostname: string
|
||||
expires: Date,
|
||||
isUpcomingExpire: boolean
|
||||
}
|
||||
|
||||
interface CacheAccount {
|
||||
accountId: string
|
||||
contacts: string[]
|
||||
hostnames: CacheAccountHostname[]
|
||||
}
|
||||
|
||||
// `app/page.tsx` is the UI for the `/` URL
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-2xl font-bold">Home</h1>
|
||||
<p>Hello, Home page!</p>
|
||||
</>
|
||||
);
|
||||
const [accounts, setAccounts] = useState<CacheAccount[]>([]);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const {
|
||||
value: newContact,
|
||||
error: contactError,
|
||||
handleChange: handleContactChange
|
||||
} = useValidation("", isValidEmail, "Invalid email format.");
|
||||
const {
|
||||
value: newHostname,
|
||||
error: hostnameError,
|
||||
handleChange: handleHostnameChange
|
||||
} = useValidation("", isValidHostname, "Invalid hostname format.");
|
||||
|
||||
const init = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (init.current)
|
||||
return;
|
||||
|
||||
const fetchAccounts = async () => {
|
||||
const newAccounts: CacheAccount[] = [];
|
||||
|
||||
const accountsResponse = await httpService.get<GetAccountsResponse>(GetApiRoute(ApiRoutes.CACHE_GET_ACCOUNTS));
|
||||
for (const accountId of accountsResponse.accountIds) {
|
||||
const contactsResponse = await httpService.get<GetContactsResponse>(GetApiRoute(ApiRoutes.CACHE_GET_CONTACTS, accountId));
|
||||
const hostnamesResponse = await httpService.get<GetHostnamesResponse>(GetApiRoute(ApiRoutes.CACHE_GET_HOSTNAMES, accountId));
|
||||
|
||||
newAccounts.push({
|
||||
accountId: accountId,
|
||||
contacts: contactsResponse.contacts,
|
||||
hostnames: hostnamesResponse.hostnames.map(h => ({
|
||||
hostname: h.hostname,
|
||||
expires: new Date(h.expires),
|
||||
isUpcomingExpire: h.isUpcomingExpire
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
setAccounts(newAccounts);
|
||||
};
|
||||
|
||||
fetchAccounts();
|
||||
init.current = true;
|
||||
}, []);
|
||||
|
||||
const deleteAccount = (accountId: string) => {
|
||||
setAccounts(accounts.filter(account => account.accountId !== accountId));
|
||||
}
|
||||
|
||||
const deleteContact = (accountId: string, contact: string) => {
|
||||
setAccounts(accounts.map(account =>
|
||||
account.accountId === accountId
|
||||
? { ...account, contacts: account.contacts.filter(c => c !== contact) }
|
||||
: account
|
||||
));
|
||||
}
|
||||
|
||||
const addContact = (accountId: string) => {
|
||||
if (newContact.trim() === "" || contactError) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAccounts(accounts.map(account =>
|
||||
account.accountId === accountId
|
||||
? { ...account, contacts: [...account.contacts, newContact.trim()] }
|
||||
: account
|
||||
));
|
||||
handleContactChange("");
|
||||
}
|
||||
|
||||
const deleteHostname = (accountId: string, hostname: string) => {
|
||||
setAccounts(accounts.map(account =>
|
||||
account.accountId === accountId
|
||||
? { ...account, hostnames: account.hostnames.filter(h => h.hostname !== hostname) }
|
||||
: account
|
||||
));
|
||||
}
|
||||
|
||||
const addHostname = (accountId: string) => {
|
||||
if (newHostname.trim() === "" || hostnameError) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAccounts(accounts.map(account =>
|
||||
account.accountId === accountId
|
||||
? { ...account, hostnames: [...account.hostnames, { hostname: newHostname.trim(), expires: new Date(), isUpcomingExpire: false }] }
|
||||
: account
|
||||
));
|
||||
handleHostnameChange("");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode) {
|
||||
handleContactChange(newContact);
|
||||
handleHostnameChange(newHostname);
|
||||
}
|
||||
}, [isEditMode, newContact, newHostname]);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-center">LetsEncrypt Client Dashboard</h1>
|
||||
<button
|
||||
onClick={() => setIsEditMode(!isEditMode)}
|
||||
className="bg-blue-500 text-white px-3 py-1 rounded">
|
||||
{isEditMode ? "View Mode" : "Edit Mode"}
|
||||
</button>
|
||||
</div>
|
||||
{
|
||||
accounts.map(account => (
|
||||
<div key={account.accountId} className="bg-white shadow-lg rounded-lg p-6 mb-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-semibold">Account: {account.accountId}</h2>
|
||||
{isEditMode && (
|
||||
<button
|
||||
onClick={() => deleteAccount(account.accountId)}
|
||||
className="bg-red-500 text-white px-3 py-1 rounded h-10">
|
||||
Delete Account
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{
|
||||
account.contacts.map(contact => (
|
||||
<li key={contact} className="text-gray-700 flex justify-between items-center mb-2">
|
||||
{contact}
|
||||
{isEditMode && (
|
||||
<button
|
||||
onClick={() => deleteContact(account.accountId, contact)}
|
||||
className="bg-red-500 text-white px-2 py-1 rounded ml-4 h-10">
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
{isEditMode && (
|
||||
<div className="flex mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={newContact}
|
||||
onChange={(e) => handleContactChange(e.target.value)}
|
||||
className="border p-2 rounded mr-2 flex-grow h-10"
|
||||
placeholder="Add new contact"
|
||||
/>
|
||||
<button
|
||||
onClick={() => addContact(account.accountId)}
|
||||
className="bg-blue-500 text-white px-3 py-1 rounded h-10">
|
||||
Add Contact
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isEditMode && contactError && <p className="text-red-500">{contactError}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{
|
||||
account.hostnames.map(hostname => (
|
||||
<li key={hostname.hostname} className="text-gray-700 flex justify-between items-center mb-2">
|
||||
<div>
|
||||
{hostname.hostname} - {hostname.expires.toDateString()} -
|
||||
<span className={`ml-2 px-2 py-1 rounded ${hostname.isUpcomingExpire ? 'bg-yellow-200 text-yellow-800' : 'bg-green-200 text-green-800'}`}>
|
||||
{hostname.isUpcomingExpire ? 'Upcoming' : 'Not Upcoming'}
|
||||
</span>
|
||||
</div>
|
||||
{isEditMode && (
|
||||
<button
|
||||
onClick={() => deleteHostname(account.accountId, hostname.hostname)}
|
||||
className="bg-red-500 text-white px-2 py-1 rounded ml-4 h-10">
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
{isEditMode && (
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={newHostname}
|
||||
onChange={(e) => handleHostnameChange(e.target.value)}
|
||||
className="border p-2 rounded mr-2 flex-grow h-10"
|
||||
placeholder="Add new hostname"
|
||||
/>
|
||||
<button
|
||||
onClick={() => addHostname(account.accountId)}
|
||||
className="bg-blue-500 text-white px-3 py-1 rounded h-10">
|
||||
Add Hostname
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isEditMode && hostnameError && <p className="text-red-500">{hostnameError}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,8 +5,8 @@ const Footer = () => {
|
||||
<footer className="bg-gray-900 text-white text-center p-4">
|
||||
<p>© {new Date().getFullYear()} MAKS-IT</p>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Footer
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import './loader.css'; // Add your loader styles here
|
||||
import React from 'react'
|
||||
import './loader.css' // Add your loader styles here
|
||||
|
||||
const Loader: React.FC = () => {
|
||||
return (
|
||||
@ -7,7 +7,7 @@ const Loader: React.FC = () => {
|
||||
<div className="spinner"></div>
|
||||
<div className="loading-text">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Loader;
|
||||
export default Loader
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
interface OffCanvasProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const OffCanvas: FC<OffCanvasProps> = ({ isOpen, onClose }) => {
|
||||
@ -26,9 +26,9 @@ const OffCanvas: FC<OffCanvasProps> = ({ isOpen, onClose }) => {
|
||||
{/* Your off-canvas content goes here */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
OffCanvas
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
"use client"; // Add this line
|
||||
"use client" // Add this line
|
||||
|
||||
import React, { FC, useState } from 'react';
|
||||
import { FaCog, FaBars } from 'react-icons/fa';
|
||||
import Link from 'next/link';
|
||||
import React, { FC, useState } from 'react'
|
||||
import { FaCog, FaBars } from 'react-icons/fa'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface TopMenuProps {
|
||||
onToggleOffCanvas: () => void;
|
||||
onToggleOffCanvas: () => void
|
||||
}
|
||||
|
||||
const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const toggleMenu = () => {
|
||||
setIsMenuOpen(!isMenuOpen);
|
||||
};
|
||||
setIsMenuOpen(!isMenuOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="bg-gray-900 text-white flex items-center p-4">
|
||||
@ -51,9 +51,9 @@ const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
|
||||
<FaBars />
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
TopMenu
|
||||
};
|
||||
}
|
||||
|
||||
37
src/ClientApp/hooks/useValidation.tsx
Normal file
37
src/ClientApp/hooks/useValidation.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
// Helper functions for validation
|
||||
const isValidEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
const isValidHostname = (hostname: string) => {
|
||||
const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/;
|
||||
return hostnameRegex.test(hostname);
|
||||
}
|
||||
|
||||
// Custom hook for input validation
|
||||
const useValidation = (initialValue: string, validateFn: (value: string) => boolean, errorMessage: string) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
setValue(newValue);
|
||||
if (newValue.trim() === "") {
|
||||
setError("This field cannot be empty.");
|
||||
} else if (!validateFn(newValue.trim())) {
|
||||
setError(errorMessage);
|
||||
} else {
|
||||
setError("");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleChange(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
return { value, error, handleChange };
|
||||
};
|
||||
|
||||
export { useValidation, isValidEmail, isValidHostname };
|
||||
@ -1,12 +0,0 @@
|
||||
import { HttpService } from "./services/HttpService";
|
||||
|
||||
const httpService = new HttpService();
|
||||
|
||||
httpService.addRequestInterceptor(xhr => {
|
||||
|
||||
});
|
||||
|
||||
httpService.addResponseInterceptor(response => {
|
||||
|
||||
return response;
|
||||
});
|
||||
3
src/ClientApp/models/letsEncryptServer/cache/GetAccountsResponse.ts
vendored
Normal file
3
src/ClientApp/models/letsEncryptServer/cache/GetAccountsResponse.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export interface GetAccountsResponse {
|
||||
accountIds: string[]
|
||||
}
|
||||
3
src/ClientApp/models/letsEncryptServer/cache/GetContactsResponse.ts
vendored
Normal file
3
src/ClientApp/models/letsEncryptServer/cache/GetContactsResponse.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export interface GetContactsResponse {
|
||||
contacts: string[]
|
||||
}
|
||||
9
src/ClientApp/models/letsEncryptServer/cache/GetHostnamesResponse.ts
vendored
Normal file
9
src/ClientApp/models/letsEncryptServer/cache/GetHostnamesResponse.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
interface HostnameResponse {
|
||||
hostname: string
|
||||
expires: string,
|
||||
isUpcomingExpire: boolean
|
||||
}
|
||||
|
||||
export interface GetHostnamesResponse {
|
||||
hostnames: HostnameResponse[]
|
||||
}
|
||||
@ -92,19 +92,19 @@ class HttpService {
|
||||
});
|
||||
}
|
||||
|
||||
public get<TResponse>(url: string): Promise<TResponse | ProblemDetails> {
|
||||
public get<TResponse>(url: string): Promise<TResponse> {
|
||||
return this.request<TResponse>('GET', url);
|
||||
}
|
||||
|
||||
public post<TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | ProblemDetails> {
|
||||
public post<TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse> {
|
||||
return this.request<TResponse>('POST', url, data);
|
||||
}
|
||||
|
||||
public put<TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | ProblemDetails> {
|
||||
public put<TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse> {
|
||||
return this.request<TResponse>('PUT', url, data);
|
||||
}
|
||||
|
||||
public delete<TResponse>(url: string): Promise<TResponse | ProblemDetails> {
|
||||
public delete<TResponse>(url: string): Promise<TResponse> {
|
||||
return this.request<TResponse>('DELETE', url);
|
||||
}
|
||||
|
||||
@ -112,11 +112,24 @@ class HttpService {
|
||||
this.requestInterceptors.push(interceptor);
|
||||
}
|
||||
|
||||
public addResponseInterceptor<TResponse>(interceptor: ResponseInterceptor<TResponse | ProblemDetails>): void {
|
||||
public addResponseInterceptor<TResponse>(interceptor: ResponseInterceptor<TResponse>): void {
|
||||
this.responseInterceptors.push(interceptor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const httpService = new HttpService();
|
||||
|
||||
httpService.addRequestInterceptor(xhr => {
|
||||
|
||||
});
|
||||
|
||||
httpService.addResponseInterceptor(response => {
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
|
||||
export {
|
||||
HttpService
|
||||
};
|
||||
httpService
|
||||
}
|
||||
|
||||
@ -30,13 +30,13 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
||||
while (!stoppingToken.IsCancellationRequested) {
|
||||
_logger.LogInformation("Background service is running.");
|
||||
|
||||
var (accountIds, getAccountIdsResult) = await _cacheService.ListCachedAccountsAsync();
|
||||
if (!getAccountIdsResult.IsSuccess || accountIds == null) {
|
||||
var (accountsResponse, getAccountIdsResult) = await _cacheService.GetAccountsAsync();
|
||||
if (!getAccountIdsResult.IsSuccess || accountsResponse == null) {
|
||||
LogErrors(getAccountIdsResult.Errors);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var accountId in accountIds) {
|
||||
foreach (var accountId in accountsResponse.AccountIds) {
|
||||
await ProcessAccountAsync(accountId);
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ public class CacheController {
|
||||
|
||||
[HttpGet("[action]")]
|
||||
public async Task<IActionResult> GetAccounts() {
|
||||
var result = await _cacheService.ListCachedAccountsAsync();
|
||||
var result = await _cacheService.GetAccountsAsync();
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
@ -45,5 +45,11 @@ public class CacheController {
|
||||
var result = await _cacheService.SetContactsAsync(accountId, requestData);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
[HttpGet("[action]/{accountId}")]
|
||||
public async Task<IActionResult> GetHostnames(Guid accountId) {
|
||||
var result = await _cacheService.GetHostnames(accountId);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
using System.Text.Json;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using DomainResults.Common;
|
||||
using MaksIT.Core.Extensions;
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.Models.LetsEncryptServer.Cache.Requests;
|
||||
using Models.LetsEncryptServer.Cache.Responses;
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
@ -11,9 +14,11 @@ public interface ICacheService {
|
||||
Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId);
|
||||
Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
|
||||
Task<IDomainResult> DeleteFromCacheAsync(Guid accountId);
|
||||
Task<(Guid[]?, IDomainResult)> ListCachedAccountsAsync();
|
||||
Task<(string[]?, IDomainResult)> GetContactsAsync(Guid accountId);
|
||||
Task<(GetAccountsResponse?, IDomainResult)> GetAccountsAsync();
|
||||
Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId);
|
||||
Task<IDomainResult> SetContactsAsync(Guid accountId, SetContactsRequest requestData);
|
||||
|
||||
Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
|
||||
}
|
||||
|
||||
public class CacheService : ICacheService, IDisposable {
|
||||
@ -121,35 +126,41 @@ public class CacheService : ICacheService, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(Guid[]?, IDomainResult)> ListCachedAccountsAsync() {
|
||||
public async Task<(GetAccountsResponse?, IDomainResult)> GetAccountsAsync() {
|
||||
await _cacheLock.WaitAsync();
|
||||
|
||||
try {
|
||||
var cacheFiles = Directory.GetFiles(_cacheDirectory);
|
||||
if (cacheFiles == null)
|
||||
return IDomainResult.Success(new Guid[0]);
|
||||
return IDomainResult.Success(new GetAccountsResponse {
|
||||
AccountIds = Array.Empty<Guid>()
|
||||
});
|
||||
|
||||
var accountIds = cacheFiles.Select(x => Path.GetFileNameWithoutExtension(x).ToGuid()).ToArray();
|
||||
|
||||
return IDomainResult.Success(accountIds);
|
||||
return IDomainResult.Success(new GetAccountsResponse {
|
||||
AccountIds = accountIds
|
||||
});
|
||||
}
|
||||
catch (Exception ex) {
|
||||
var message = "Error listing cache files";
|
||||
_logger.LogError(ex, message);
|
||||
|
||||
return IDomainResult.Failed<Guid[]?> (message);
|
||||
return IDomainResult.Failed<GetAccountsResponse?> (message);
|
||||
}
|
||||
finally {
|
||||
_cacheLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(string[]?, IDomainResult)> GetContactsAsync(Guid accountId) {
|
||||
public async Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId) {
|
||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
||||
if (!loadResult.IsSuccess || cache == null)
|
||||
return (null, loadResult);
|
||||
|
||||
return IDomainResult.Success(cache.Contacts);
|
||||
return IDomainResult.Success(new GetContactsResponse {
|
||||
Contacts = cache.Contacts ?? Array.Empty<string>()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -162,6 +173,33 @@ public class CacheService : ICacheService, IDisposable {
|
||||
return await SaveToCacheAsync(accountId, cache);
|
||||
}
|
||||
|
||||
public async Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId) {
|
||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
||||
if (!loadResult.IsSuccess || cache?.CachedCerts == null)
|
||||
return (null, loadResult);
|
||||
|
||||
var hoststWithUpcomingSslExpire = cache.GetHostsWithUpcomingSslExpiry();
|
||||
|
||||
|
||||
var response = new GetHostnamesResponse {
|
||||
Hostnames = new List<HostnameResponse>()
|
||||
};
|
||||
|
||||
foreach (var result in cache.CachedCerts) {
|
||||
var (subject, cachedChert) = result;
|
||||
|
||||
var cert = new X509Certificate2(Encoding.ASCII.GetBytes(cachedChert.Cert));
|
||||
|
||||
response.Hostnames.Add(new HostnameResponse {
|
||||
Hostname = subject,
|
||||
Expires = cert.NotBefore,
|
||||
IsUpcomingExpire = hoststWithUpcomingSslExpire.Contains(subject)
|
||||
});
|
||||
}
|
||||
|
||||
return IDomainResult.Success(response);
|
||||
}
|
||||
|
||||
|
||||
public void Dispose() {
|
||||
_cacheLock?.Dispose();
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Models.LetsEncryptServer.Cache.Responses {
|
||||
public class GetAccountsResponse {
|
||||
public Guid[] AccountIds { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Models.LetsEncryptServer.Cache.Responses {
|
||||
public class GetContactsResponse {
|
||||
public string[] Contacts { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Models.LetsEncryptServer.Cache.Responses {
|
||||
|
||||
public class HostnameResponse {
|
||||
public string Hostname { get; set; }
|
||||
public DateTime Expires { get; set; }
|
||||
public bool IsUpcomingExpire { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class GetHostnamesResponse {
|
||||
public List<HostnameResponse> Hostnames { get; set; }
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Agent\Responses\" />
|
||||
<Folder Include="LetsEncryptServer\Cache\Responses\" />
|
||||
<Folder Include="LetsEncryptServer\CertsFlow\Responses\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user