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 {
|
enum ApiRoutes {
|
||||||
|
|
||||||
CACHE_GET_ACCOUNTS = `/api/Cache/GetAccounts`,
|
CACHE_GET_ACCOUNTS = `api/Cache/GetAccounts`,
|
||||||
CACHE_GET_CONTACTS = `/api/Cache/GetContacts/{accountId}`,
|
CACHE_GET_CONTACTS = `api/Cache/GetContacts/{accountId}`,
|
||||||
CACHE_SET_CONTACTS = `/api/Cache/SetContacts/{accountId}`,
|
CACHE_SET_CONTACTS = `api/Cache/SetContacts/{accountId}`,
|
||||||
|
CACHE_GET_HOSTNAMES = `api/Cache/GetHostnames/{accountId}`,
|
||||||
|
|
||||||
CERTS_FLOW_CONFIGURE_CLIENT = `/api/CertsFlow/ConfigureClient`,
|
CERTS_FLOW_CONFIGURE_CLIENT = `api/CertsFlow/ConfigureClient`,
|
||||||
CERTS_FLOW_TERMS_OF_SERVICE = `/api/CertsFlow/TermsOfService/{sessionId}`,
|
CERTS_FLOW_TERMS_OF_SERVICE = `api/CertsFlow/TermsOfService/{sessionId}`,
|
||||||
CERTS_FLOW_INIT = `/api/CertsFlow/Init/{sessionId}/{accountId}`,
|
CERTS_FLOW_INIT = `api/CertsFlow/Init/{sessionId}/{accountId}`,
|
||||||
CERTS_FLOW_NEW_ORDER = `/api/CertsFlow/NewOrder/{sessionId}`,
|
CERTS_FLOW_NEW_ORDER = `api/CertsFlow/NewOrder/{sessionId}`,
|
||||||
CERTS_FLOW_GET_ORDER = `/api/CertsFlow/GetOrder/{sessionId}`,
|
CERTS_FLOW_GET_ORDER = `api/CertsFlow/GetOrder/{sessionId}`,
|
||||||
CERTS_FLOW_GET_CERTIFICATES = `/api/CertsFlow/GetCertificates/{sessionId}`,
|
CERTS_FLOW_GET_CERTIFICATES = `api/CertsFlow/GetCertificates/{sessionId}`,
|
||||||
CERTS_FLOW_APPLY_CERTIFICATES = `/api/CertsFlow/ApplyCertificates/{sessionId}`,
|
CERTS_FLOW_APPLY_CERTIFICATES = `api/CertsFlow/ApplyCertificates/{sessionId}`,
|
||||||
CERTS_FLOW_HOSRS_WITH_UPCOMING_SSL_EXPIRY = `/api/CertsFlow/HostsWithUpcomingSslExpiry/{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
|
// `app/page.tsx` is the UI for the `/` URL
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<div className="container mx-auto p-4">
|
||||||
<h1 className="text-2xl font-bold">Home</h1>
|
<div className="flex justify-between items-center mb-8">
|
||||||
<p>Hello, Home page!</p>
|
<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">
|
<footer className="bg-gray-900 text-white text-center p-4">
|
||||||
<p>© {new Date().getFullYear()} MAKS-IT</p>
|
<p>© {new Date().getFullYear()} MAKS-IT</p>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Footer
|
Footer
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import './loader.css'; // Add your loader styles here
|
import './loader.css' // Add your loader styles here
|
||||||
|
|
||||||
const Loader: React.FC = () => {
|
const Loader: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@ -7,7 +7,7 @@ const Loader: React.FC = () => {
|
|||||||
<div className="spinner"></div>
|
<div className="spinner"></div>
|
||||||
<div className="loading-text">Loading...</div>
|
<div className="loading-text">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Loader;
|
export default Loader
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
interface OffCanvasProps {
|
interface OffCanvasProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const OffCanvas: FC<OffCanvasProps> = ({ isOpen, onClose }) => {
|
const OffCanvas: FC<OffCanvasProps> = ({ isOpen, onClose }) => {
|
||||||
@ -26,9 +26,9 @@ const OffCanvas: FC<OffCanvasProps> = ({ isOpen, onClose }) => {
|
|||||||
{/* Your off-canvas content goes here */}
|
{/* Your off-canvas content goes here */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
OffCanvas
|
OffCanvas
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
"use client"; // Add this line
|
"use client" // Add this line
|
||||||
|
|
||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useState } from 'react'
|
||||||
import { FaCog, FaBars } from 'react-icons/fa';
|
import { FaCog, FaBars } from 'react-icons/fa'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link'
|
||||||
|
|
||||||
interface TopMenuProps {
|
interface TopMenuProps {
|
||||||
onToggleOffCanvas: () => void;
|
onToggleOffCanvas: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
|
const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
const toggleMenu = () => {
|
const toggleMenu = () => {
|
||||||
setIsMenuOpen(!isMenuOpen);
|
setIsMenuOpen(!isMenuOpen)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-gray-900 text-white flex items-center p-4">
|
<header className="bg-gray-900 text-white flex items-center p-4">
|
||||||
@ -51,9 +51,9 @@ const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
|
|||||||
<FaBars />
|
<FaBars />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
TopMenu
|
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);
|
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);
|
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);
|
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);
|
return this.request<TResponse>('DELETE', url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,11 +112,24 @@ class HttpService {
|
|||||||
this.requestInterceptors.push(interceptor);
|
this.requestInterceptors.push(interceptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addResponseInterceptor<TResponse>(interceptor: ResponseInterceptor<TResponse | ProblemDetails>): void {
|
public addResponseInterceptor<TResponse>(interceptor: ResponseInterceptor<TResponse>): void {
|
||||||
this.responseInterceptors.push(interceptor);
|
this.responseInterceptors.push(interceptor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const httpService = new HttpService();
|
||||||
|
|
||||||
|
httpService.addRequestInterceptor(xhr => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
httpService.addResponseInterceptor(response => {
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
HttpService
|
httpService
|
||||||
};
|
}
|
||||||
|
|||||||
@ -30,13 +30,13 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
|||||||
while (!stoppingToken.IsCancellationRequested) {
|
while (!stoppingToken.IsCancellationRequested) {
|
||||||
_logger.LogInformation("Background service is running.");
|
_logger.LogInformation("Background service is running.");
|
||||||
|
|
||||||
var (accountIds, getAccountIdsResult) = await _cacheService.ListCachedAccountsAsync();
|
var (accountsResponse, getAccountIdsResult) = await _cacheService.GetAccountsAsync();
|
||||||
if (!getAccountIdsResult.IsSuccess || accountIds == null) {
|
if (!getAccountIdsResult.IsSuccess || accountsResponse == null) {
|
||||||
LogErrors(getAccountIdsResult.Errors);
|
LogErrors(getAccountIdsResult.Errors);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var accountId in accountIds) {
|
foreach (var accountId in accountsResponse.AccountIds) {
|
||||||
await ProcessAccountAsync(accountId);
|
await ProcessAccountAsync(accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ public class CacheController {
|
|||||||
|
|
||||||
[HttpGet("[action]")]
|
[HttpGet("[action]")]
|
||||||
public async Task<IActionResult> GetAccounts() {
|
public async Task<IActionResult> GetAccounts() {
|
||||||
var result = await _cacheService.ListCachedAccountsAsync();
|
var result = await _cacheService.GetAccountsAsync();
|
||||||
return result.ToActionResult();
|
return result.ToActionResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,5 +45,11 @@ public class CacheController {
|
|||||||
var result = await _cacheService.SetContactsAsync(accountId, requestData);
|
var result = await _cacheService.SetContactsAsync(accountId, requestData);
|
||||||
return result.ToActionResult();
|
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 DomainResults.Common;
|
||||||
using MaksIT.Core.Extensions;
|
using MaksIT.Core.Extensions;
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.Models.LetsEncryptServer.Cache.Requests;
|
using MaksIT.Models.LetsEncryptServer.Cache.Requests;
|
||||||
|
using Models.LetsEncryptServer.Cache.Responses;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services;
|
namespace MaksIT.LetsEncryptServer.Services;
|
||||||
|
|
||||||
@ -11,9 +14,11 @@ public interface ICacheService {
|
|||||||
Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId);
|
Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId);
|
||||||
Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
|
Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
|
||||||
Task<IDomainResult> DeleteFromCacheAsync(Guid accountId);
|
Task<IDomainResult> DeleteFromCacheAsync(Guid accountId);
|
||||||
Task<(Guid[]?, IDomainResult)> ListCachedAccountsAsync();
|
Task<(GetAccountsResponse?, IDomainResult)> GetAccountsAsync();
|
||||||
Task<(string[]?, IDomainResult)> GetContactsAsync(Guid accountId);
|
Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId);
|
||||||
Task<IDomainResult> SetContactsAsync(Guid accountId, SetContactsRequest requestData);
|
Task<IDomainResult> SetContactsAsync(Guid accountId, SetContactsRequest requestData);
|
||||||
|
|
||||||
|
Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CacheService : ICacheService, IDisposable {
|
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();
|
await _cacheLock.WaitAsync();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var cacheFiles = Directory.GetFiles(_cacheDirectory);
|
var cacheFiles = Directory.GetFiles(_cacheDirectory);
|
||||||
if (cacheFiles == null)
|
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();
|
var accountIds = cacheFiles.Select(x => Path.GetFileNameWithoutExtension(x).ToGuid()).ToArray();
|
||||||
|
|
||||||
return IDomainResult.Success(accountIds);
|
return IDomainResult.Success(new GetAccountsResponse {
|
||||||
|
AccountIds = accountIds
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
var message = "Error listing cache files";
|
var message = "Error listing cache files";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
|
|
||||||
return IDomainResult.Failed<Guid[]?> (message);
|
return IDomainResult.Failed<GetAccountsResponse?> (message);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
_cacheLock.Release();
|
_cacheLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(string[]?, IDomainResult)> GetContactsAsync(Guid accountId) {
|
public async Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null)
|
if (!loadResult.IsSuccess || cache == null)
|
||||||
return (null, loadResult);
|
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);
|
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() {
|
public void Dispose() {
|
||||||
_cacheLock?.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>
|
<ItemGroup>
|
||||||
<Folder Include="Agent\Responses\" />
|
<Folder Include="Agent\Responses\" />
|
||||||
<Folder Include="LetsEncryptServer\Cache\Responses\" />
|
|
||||||
<Folder Include="LetsEncryptServer\CertsFlow\Responses\" />
|
<Folder Include="LetsEncryptServer\CertsFlow\Responses\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user